diff options
210 files changed, 3537 insertions, 1062 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1dc49ca336d..85730e1b687 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -115,6 +115,11 @@ bundler:audit: script: - "bundle exec bundle-audit check --update --ignore OSVDB-115941" +db-migrate-reset: + stage: test + script: + - RAILS_ENV=test bundle exec rake db:migrate:reset + # Ruby 2.2 jobs spec:feature:ruby22: diff --git a/CHANGELOG b/CHANGELOG index 16bb1d53b2a..2d1c561fb82 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,19 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Remove future dates from contribution calendar graph. -v 8.7.0 (unreleased) +v 8.7.1 (unreleased) + - Fix .gitlab-ci.yml parsing issue when hidde job is a template without script definition. !3849 + - Fix license detection to detect all license files, not only known licenses. !3878 + - Use the `can?` helper instead of `current_user.can?`. !3882 + - Prevent users from deleting Webhooks via API they do not own + +v 8.7.0 + - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented + - Fix vulnerability that made it possible to gain access to private labels and milestones - The number of InfluxDB points stored per UDP packet can now be configured + - Fix error when cross-project label reference used with non-existent project - Transactions for /internal/allowed now have an "action" tag set - Method instrumentation now uses Module#prepend instead of aliasing methods - Repository.clean_old_archives is now instrumented @@ -19,6 +29,7 @@ v 8.7.0 (unreleased) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles) - Restrict user profiles when public visibility level is restricted. + - Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - Add setting for customizing the list of trusted proxies !3524 @@ -49,8 +60,9 @@ v 8.7.0 (unreleased) - Add endpoints to archive or unarchive a project !3372 - Fix a bug whith trailing slash in bamboo_url - Add links to CI setup documentation from project settings and builds pages + - Display project members page to all members - Handle nil descriptions in Slack issue messages (Stan Hu) - - Add automated repository integrity checks + - Add automated repository integrity checks (OFF by default) - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) - API: Ability to star and unstar a project (Robert Schilling) - Add default scope to projects to exclude projects pending deletion @@ -67,6 +79,7 @@ v 8.7.0 (unreleased) - Hide `Create a group` help block when creating a new project in a group - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Allow issues and merge requests to be assigned to the author !2765 + - Make Ci::Commit to group only similar builds and make it stateful (ref, tag) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Decouple membership and notifications - Fix creation of merge requests for orphaned branches (Stan Hu) @@ -76,6 +89,7 @@ v 8.7.0 (unreleased) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Fix admin/projects when using visibility levels on search (PotHix) - Build status notifications + - Update email confirmation interface - API: Expose user location (Robert Schilling) - API: Do not leak group existence via return code (Robert Schilling) - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 @@ -86,6 +100,10 @@ v 8.7.0 (unreleased) - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) - Improved markdown forms + - Diff design updates (colors, button styles, etc) + - Copying and pasting a diff no longer pastes the line numbers or +/- + - Add null check to formData when updating profile content to fix Firefox bug + - Disable spellcheck and autocorrect for username field in admin page - Delete tags using Rugged for performance reasons (Robert Schilling) - Add Slack notifications when Wiki is edited (Sebastian Klier) - Diffs load at the correct point when linking from from number @@ -99,6 +117,7 @@ v 8.7.0 (unreleased) - Updated print style for issues - Use GitHub Issue/PR number as iid to keep references - Import GitHub labels + - Add option to filter by "Owned projects" on dashboard page - Import GitHub milestones - Fix emoji catgories in the emoji picker - Execute system web hooks on push to the project @@ -107,7 +126,7 @@ v 8.7.0 (unreleased) - Add RAW build trace output and button on build page - Add incremental build trace update into CI API -v 8.6.7 (unreleased) +v 8.6.7 - Fix persistent XSS vulnerability in `commit_person_link` helper - Fix persistent XSS vulnerability in Label and Milestone dropdowns - Fix vulnerability that made it possible to enumerate private projects belonging to group @@ -117,7 +136,6 @@ v 8.6.6 - Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654 - Fix revoking of authorized OAuth applications (Connor Shea). !3690 - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) - - Project switcher uses new dropdown styling - Issuable header is consistent between issues and merge requests - Improved spacing in issuable header on mobile @@ -249,6 +267,9 @@ v 8.6.0 - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests +v 8.5.11 + - Fix persistent XSS vulnerability in `commit_person_link` helper + v 8.5.10 - Fix a 2FA authentication spoofing vulnerability. @@ -396,6 +417,9 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.9 + - Fix persistent XSS vulnerability in `commit_person_link` helper + v 8.4.8 - Fix a 2FA authentication spoofing vulnerability. @@ -518,6 +542,9 @@ v 8.4.0 - Add IP check against DNSBLs at account sign-up - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching +v 8.3.8 + - Fix persistent XSS vulnerability in `commit_person_link` helper + v 8.3.7 - Fix a 2FA authentication spoofing vulnerability. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f26a5d7eaf..24cd5864530 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -323,6 +323,7 @@ request is as follows: [shell command guidelines](doc/development/shell_commands.md) 1. If your code creates new files on disk please read the [shared files guidelines](doc/development/shared_files.md). +1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/). The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. This is the best time to submit an MR and get @@ -178,7 +178,7 @@ gem 'ruby-fogbugz', '~> 0.2.1' gem 'd3_rails', '~> 3.5.0' #cal-heatmap -gem 'cal-heatmap-rails', '~> 3.5.0' +gem 'cal-heatmap-rails', '~> 3.6.0' # underscore-rails gem "underscore-rails", "~> 1.8.0" diff --git a/Gemfile.lock b/Gemfile.lock index b00d7b35c84..add056b169f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,7 +103,7 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) - cal-heatmap-rails (3.5.1) + cal-heatmap-rails (3.6.0) capybara (2.6.2) addressable mime-types (>= 1.16) @@ -134,7 +134,7 @@ GEM execjs coffee-script-source (1.10.0) colorize (0.7.7) - concurrent-ruby (1.0.0) + concurrent-ruby (1.0.1) connection_pool (2.2.0) coveralls (0.8.13) json (~> 1.8) @@ -629,7 +629,7 @@ GEM recaptcha (1.0.2) json redcarpet (3.3.3) - redis (3.2.2) + redis (3.3.0) redis-actionpack (4.0.1) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -736,10 +736,9 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.0.1) + sidekiq (4.1.1) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - json (~> 1.0) redis (~> 3.2, >= 3.2.1) sidekiq-cron (0.4.0) redis-namespace (>= 1.5.2) @@ -905,7 +904,7 @@ DEPENDENCIES bullet bundler-audit byebug - cal-heatmap-rails (~> 3.5.0) + cal-heatmap-rails (~> 3.6.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.10.0) diff --git a/PROCESS.md b/PROCESS.md index cad45d23df9..e34f59c6bce 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -105,6 +105,25 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to merge requests is in the [Thoughtbot code review guide]. +## Feature Freeze + +5 working days before the 22nd the stable branches for the upcoming release will +be frozen for major changes. Merge requests may still be merged into master +during this period. By freezing the stable branches prior to a release there's +no need to worry about last minute merge requests potentially breaking a lot of +things. + +What is considered to be a major change is determined on a case by case basis as +this definition depends very much on the context of changes. For example, a 5 +line change might have a big impact on the entire application. Ultimately the +decision will be made by those reviewing a merge request and the release +manager. + +During the feature freeze all merge requests that are meant to go into the next +release should have the correct milestone assigned _and_ have the label +~"Pick into Stable" set. Merge requests without a milestone and this label will +not be merged into any stable branches. + ## Copy & paste responses ### Improperly formatted issue diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index ffd3627b1b0..0acb4c1955e 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -1,7 +1,7 @@ class @CommitsList @timer = null - @init: (ref, limit) -> + @init: (limit) -> $("body").on "click", ".day-commits-table li.commit", (event) -> if event.target.nodeName != "A" location.href = $(this).attr("url") diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 0b9110d35fa..2fdb7562515 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -17,6 +17,7 @@ class Dispatcher switch page when 'projects:issues:index' Issues.init() + Issuable.init() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() @@ -57,7 +58,7 @@ class Dispatcher new ZenMode() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() - MergeRequests.init() + Issuable.init() when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee new file mode 100644 index 00000000000..a4304786cbb --- /dev/null +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -0,0 +1,64 @@ +class @DueDateSelect + constructor: -> + $loading = $('.js-issuable-update .due_date') + .find('.block-loading') + .hide() + + $('.js-due-date-select').each (i, dropdown) -> + $dropdown = $(dropdown) + $dropdownParent = $dropdown.closest('.dropdown') + $datePicker = $dropdownParent.find('.js-due-date-calendar') + $block = $dropdown.closest('.block') + $selectbox = $dropdown.closest('.selectbox') + $value = $block.find('.value') + $sidebarValue = $('.js-due-date-sidebar-value', $block) + + fieldName = $dropdown.data('field-name') + abilityName = $dropdown.data('ability-name') + issueUpdateURL = $dropdown.data('issue-update') + + $dropdown.glDropdown( + hidden: -> + $selectbox.hide() + $value.removeAttr('style') + ) + + addDueDate = -> + # Create the post date + value = $("input[name='#{fieldName}']").val() + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + + data = {} + data[abilityName] = {} + data[abilityName].due_date = value + + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + beforeSend: -> + $loading.fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() + $value.removeAttr('style') + + $value.html(mediumDate) + $sidebarValue.html(mediumDate) + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') + $loading.fadeOut() + + $datePicker.datepicker( + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='#{fieldName}']").val() + altField: "input[name='#{fieldName}']" + onSelect: -> + addDueDate() + ) + + $(document) + .off 'click', '.ui-datepicker-header a' + .on 'click', '.ui-datepicker-header a', (e) -> + e.stopImmediatePropagation() diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 641141072f4..29466e9f2ed 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -221,6 +221,9 @@ class GitLabDropdown menu.toggleClass PAGE_TWO_CLASS + # Focus first visible input on active page + @dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus() + parseData: (data) -> @renderedData = data @@ -240,7 +243,8 @@ class GitLabDropdown shouldPropagate: (e) => if @options.multiSelect $target = $(e.target) - if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') + + if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link') e.stopPropagation() return false else @@ -375,7 +379,6 @@ class GitLabDropdown selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - if el.hasClass(ACTIVE_CLASS) el.removeClass(ACTIVE_CLASS) field.remove() @@ -386,13 +389,13 @@ class GitLabDropdown else selectedObject else - if !value? - field.remove() - - if not @options.multiSelect + if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.parent().find("input[name='#{fieldName}']").remove() + if !value? + field.remove() + # Toggle active class for the tick mark el.addClass ACTIVE_CLASS diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee new file mode 100644 index 00000000000..afffed63ac5 --- /dev/null +++ b/app/assets/javascripts/issuable.js.coffee @@ -0,0 +1,84 @@ +@Issuable = + init: -> + Issuable.initTemplates() + Issuable.initSearch() + + initTemplates: -> + Issuable.labelRow = _.template( + '<% _.each(labels, function(label){ %> + <span class="label-row"> + <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a> + </span> + <% }); %>' + ) + + initSearch: -> + @timer = null + $('#issue_search') + .off 'keyup' + .on 'keyup', -> + clearTimeout(@timer) + @timer = setTimeout( -> + Issuable.filterResults $('#issue_search_form') + , 500) + + toggleLabelFilters: -> + $filteredLabels = $('.filtered-labels') + if $filteredLabels.find('.label-row').length > 0 + $filteredLabels.removeClass('hidden') + else + $filteredLabels.addClass('hidden') + + filterResults: (form) => + formData = form.serialize() + + $('.issues-holder, .merge-requests-holder').css('opacity', '0.5') + formAction = form.attr('action') + issuesUrl = formAction + issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") + issuesUrl += formData + $.ajax + type: 'GET' + url: formAction + data: formData + complete: -> + $('.issues-holder, .merge-requests-holder').css('opacity', '1.0') + success: (data) -> + $('.issues-holder, .merge-requests-holder').html(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: issuesUrl}, document.title, issuesUrl + Issuable.reload() + Issuable.updateStateFilters() + $filteredLabels = $('.filtered-labels') + + if typeof Issuable.labelRow is 'function' + $filteredLabels.html(Issuable.labelRow(data)) + + Issuable.toggleLabelFilters() + + dataType: "json" + + reload: -> + if Issues.created + Issues.initChecks() + + $('#filter_issue_search').val($('#issue_search').val()) + + updateStateFilters: -> + stateFilters = $('.issues-state-filters') + newParams = {} + paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search'] + + for paramKey in paramKeys + newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' + + if stateFilters.length + stateFilters.find('a').each -> + initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]') + labelNameValues = gl.utils.getParameterValues('label_name[]') + if labelNameValues + labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&') + newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}" + else + newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) + $(this).attr 'href', newUrl diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 9ae0cd06039..3a439b94c59 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -33,7 +33,6 @@ class @IssuableContext $block.find('.dropdown-menu-toggle').trigger 'click' , 0 - $(".right-sidebar").niceScroll() initParticipants: -> diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 0d9f2094c2a..3330e6c68ad 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -1,6 +1,6 @@ @Issues = init: -> - Issues.initSearch() + Issues.created = true Issues.initChecks() $("body").on "ajax:success", ".close_issue, .reopen_issue", -> @@ -15,10 +15,6 @@ else $(this).html totalIssues - 1 - reload: -> - Issues.initChecks() - $('#filter_issue_search').val($('#issue_search').val()) - initChecks: -> $(".check_all_issues").click -> $(".selected_issue").prop("checked", @checked) @@ -26,51 +22,6 @@ $(".selected_issue").bind "change", Issues.checkChanged - # Update state filters if present in page - updateStateFilters: -> - stateFilters = $('.issues-state-filters') - newParams = {} - paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] - - for paramKey in paramKeys - newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or '' - - if stateFilters.length - stateFilters.find('a').each -> - initialUrl = $(this).attr 'href' - $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl) - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout( -> - Issues.filterResults $("#issue_search_form") - , 500) - - filterResults: (form) => - $('.issues-holder, .merge-requests-holder').css("opacity", '0.5') - formAction = form.attr('action') - formData = form.serialize() - issuesUrl = formAction - issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}") - issuesUrl += formData - - $.ajax - type: "GET" - url: formAction - data: formData - complete: -> - $('.issues-holder, .merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.issues-holder, .merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issuesUrl}, document.title, issuesUrl - Issues.reload() - Issues.updateStateFilters() - dataType: "json" - checkChanged: -> checked_issues = $(".selected_issue:checked") if checked_issues.length > 0 diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 1131492b7ae..85517b18c5a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,7 +6,7 @@ class @LabelsSelect labelUrl = $dropdown.data('labels') issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel? + if selectedLabel? and not $dropdown.hasClass 'js-multiselect' selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') @@ -16,25 +16,22 @@ class @LabelsSelect abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') + $form = $dropdown.closest('form') $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') $value = $block.find('.value') - $loading = $block.find('.block-loading').fadeOut() - - if newLabelField.length - $newLabelCreateButton = $('.js-new-label-btn') - $colorPreview = $('.js-dropdown-label-color-preview') - $newLabelError = $dropdown.parent().find('.js-label-error') - $newLabelError.hide() + $newLabelError = $('.js-label-error') + $colorPreview = $('.js-dropdown-label-color-preview') + $newLabelCreateButton = $('.js-new-label-btn') - # Suggested colors in the dropdown to chose from pre-chosen colors - $('.suggest-colors-dropdown a').on 'click', (e) -> + $newLabelError.hide() + $loading = $block.find('.block-loading').fadeOut() issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>"> - <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>;"> + <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>; color: <%= label.text_color %>;"> <%= _.escape(label.title) %> </span> </a> @@ -42,7 +39,9 @@ class @LabelsSelect ) labelNoneHTMLTemplate = _.template('<div class="light">None</div>') - if newLabelField.length and $dropdown.hasClass 'js-extra-options' + if newLabelField.length + + # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on "click", (e) -> e.preventDefault() e.stopPropagation() @@ -81,26 +80,25 @@ class @LabelsSelect enableLabelCreateButton = -> if newLabelField.val() isnt '' and newColorField.val() isnt '' $newLabelError.hide() - $('.js-new-label-btn').disable() - - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $('.js-new-label-btn').enable() - - if label.message? - $newLabelError - .text label.message - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - $newLabelCreateButton.enable() else $newLabelCreateButton.disable() + saveLabel = -> + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $newLabelCreateButton.enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + newLabelField.on 'keyup change', enableLabelCreateButton newColorField.on 'keyup change', enableLabelCreateButton @@ -111,24 +109,7 @@ class @LabelsSelect .on 'click', (e) -> e.preventDefault() e.stopPropagation() - - if newLabelField.val() isnt '' and newColorField.val() isnt '' - $newLabelError.hide() - $('.js-new-label-btn').disable() - - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $('.js-new-label-btn').enable() - - if label.message? - $newLabelError - .text label.message - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + saveLabel() saveLabelData = -> selected = $dropdown @@ -171,7 +152,7 @@ class @LabelsSelect .find('a') .each((i) -> setTimeout(=> - glAnimate($(@), 'pulse') + gl.animate.animate($(@), 'pulse') ,200 * i ) ) @@ -200,16 +181,21 @@ class @LabelsSelect callback data renderRow: (label) -> - selectedClass = '' - if $selectbox.find("input[type='hidden']\ - [name='#{$dropdown.data('field-name')}']\ - [value='#{label.id}']").length - selectedClass = 'is-active' + removesAll = label.id is 0 or not label.id? + + selectedClass = [] + if $form.find("input[type='hidden']\ + [name='#{$dropdown.data('fieldName')}']\ + [value='#{this.id(label)}']").length + selectedClass.push 'is-active' + + if $dropdown.hasClass('js-multiselect') and removesAll + selectedClass.push 'dropdown-clear-active' color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" "<li> - <a href='#' class='#{selectedClass}'> + <a href='#' class='#{selectedClass.join(' ')}'> #{color} #{_.escape(label.title)} </a> @@ -219,37 +205,56 @@ class @LabelsSelect fields: ['title'] selectable: true - toggleLabel: (selected) -> + toggleLabel: (selected, el) -> + selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') + if selected and selected.title? - selected.title + if selected_labels.length > 1 + "#{selected.title} +#{selected_labels.length - 1} more" + else + selected.title + else if not selected and selected_labels.length isnt 0 + if selected_labels.length > 1 + "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more" + else if selected_labels.length is 1 + $(selected_labels).text() else defaultLabel fieldName: $dropdown.data('field-name') id: (label) -> - if label.isAny? - '' - else if $dropdown.hasClass "js-filter-submit" - label.title + if $dropdown.hasClass("js-filter-submit") and not label.isAny? + _.escape label.title else label.id hidden: -> + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is 'projects:merge_requests:index' + $selectbox.hide() # display:block overrides the hide-collapse rule $value.removeAttr('style') if $dropdown.hasClass 'js-multiselect' - saveLabelData() + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='#{$dropdown.data('fieldName')}']") + Issuable.filterResults $dropdown.closest('form') + else if $dropdown.hasClass('js-filter-submit') + $dropdown.closest('form').submit() + else + saveLabelData() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - + isMRIndex = page is 'projects:merge_requests:index' if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedLabel = label.title - - Issues.filterResults $dropdown.closest('form') + if not $dropdown.hasClass 'js-multiselect' + selectedLabel = label.title + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee index 8f892b5a2b9..ec3b44d6126 100644 --- a/app/assets/javascripts/lib/animate.js.coffee +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -1,13 +1,39 @@ ((w) -> + if not w.gl? then w.gl = {} + if not gl.animate? then gl.animate = {} - w.glAnimate = ($el, animation, done) -> + gl.animate.animate = ($el, animation, options, done) -> + if options?.cssStart? + $el.css(options.cssStart) $el - .removeClass() + .removeClass(animation + ' animated') .addClass(animation + ' animated') .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass() + $(this).removeClass(animation + ' animated') + if done? + done() + if options?.cssEnd? + $el.css(options.cssEnd) return return - return + gl.animate.animateEach = ($els, animation, time, options, done) -> + dfd = $.Deferred() + if not $els.length + dfd.resolve() + $els.each((i) -> + setTimeout(=> + $this = $(@) + gl.animate.animate($this, animation, options, => + if i is $els.length - 1 + dfd.resolve() + if done? + done() + ) + ,time * i + ) + return + ) + return dfd.promise() + return ) window
\ No newline at end of file diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee index abd556e0b4e..6a00932c028 100644 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -3,16 +3,20 @@ w.gl ?= {} w.gl.utils ?= {} - w.gl.utils.getUrlParameter = (sParam) -> + # Returns an array containing the value(s) of the + # of the key passed as an argument + w.gl.utils.getParameterValues = (sParam) -> sPageURL = decodeURIComponent(window.location.search.substring(1)) sURLVariables = sPageURL.split('&') sParameterName = undefined + values = [] i = 0 while i < sURLVariables.length sParameterName = sURLVariables[i].split('=') if sParameterName[0] is sParam - return if sParameterName[1] is undefined then true else sParameterName[1] + values.push(sParameterName[1]) i++ + values # # # @param {Object} params - url keys and value to merge @@ -28,4 +32,12 @@ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" newUrl + # removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = (url, param) -> + url = decodeURIComponent(url) + urlVariables = url.split('&') + ( + variables for variables in urlVariables when variables.indexOf(param) is -1 + ).join('&') + ) window diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index e8613cab72b..372732d0aac 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -87,8 +87,8 @@ class @MergeRequestTabs if window.location.hash navBarHeight = $('.navbar-gitlab').outerHeight() - $el = $("#{container} #{window.location.hash}") - $.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length + $el = $("#{container} #{window.location.hash}:not(.match)") + $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length # Activate a tab based on the current action activateTab: (action) -> @@ -176,12 +176,12 @@ class @MergeRequestTabs if locationHash isnt '' hashClassString = ".#{locationHash.replace('#', '')}" - $diffLine = $(locationHash) + $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) - if $diffLine.is ':not(tr)' - $diffLine = $("td#{locationHash}, td#{hashClassString}") + if not $diffLine.is 'tr' + $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") else - $diffLine = $('td', $diffLine) + $diffLine = $diffLine.find('td') if $diffLine.length $diffLine.addClass 'hll' diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee deleted file mode 100644 index b3c73ffce5d..00000000000 --- a/app/assets/javascripts/merge_requests.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -# -# * Filter merge requests -# -@MergeRequests = - init: -> - MergeRequests.initSearch() - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout(MergeRequests.filterResults, 500) - - filterResults: => - form = $("#issue_search_form") - search = $("#issue_search").val() - $('.merge-requests-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '?' + form.serialize() - - $.ajax - type: "GET" - url: form.attr('action') - data: form.serialize() - complete: -> - $('.merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issues_url}, document.title, issues_url - MergeRequests.reload() - dataType: "json" - - reload: -> - $('#filter_issue_search').val($('#issue_search').val()) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 04fd5cf37bd..345a0e447af 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -97,7 +97,7 @@ class @MilestoneSelect selectedMilestone = selected.name else selectedMilestone = '' - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index 00d2b641723..10bef96f43d 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -1,5 +1,11 @@ class @Todos - constructor: (@name) -> + constructor: (opts = {}) -> + { + @el = $('.js-todos-options') + } = opts + + @perPage = @el.data('perPage') + @clearListeners() @initBtnListeners() @@ -26,6 +32,7 @@ class @Todos dataType: 'json' data: '_method': 'delete' success: (data) => + @redirectIfNeeded data.count @clearDone $this.closest('li') @updateBadges data @@ -57,11 +64,46 @@ class @Todos $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + getTotalPages: -> + @el.data('totalPages') + + getCurrentPage: -> + @el.data('currentPage') + + getTodosPerPage: -> + @el.data('perPage') + + redirectIfNeeded: (total) -> + currPages = @getTotalPages() + currPage = @getCurrentPage() + + # Refresh if no remaining Todos + if not total + location.reload() + return + + # Do nothing if no pagination + return if not currPages + + newPages = Math.ceil(total / @getTodosPerPage()) + url = location.href # Includes query strings + + # If new total of pages is different than we have now + if newPages isnt currPages + # Redirect to previous page if there's one available + if currPages > 1 and currPage is currPages + pageParams = + page: currPages - 1 + url = gl.utils.mergeUrlParams(pageParams, url) + + Turbolinks.visit(url) + goToTodoUrl: (e)-> todoLink = $(this).data('url') return unless todoLink - if e.metaKey + # Allow Meta-Click or Mouse3-click to open in a new tab + if e.metaKey or e.which is 2 e.preventDefault() window.open(todoLink,'_blank') else diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index a7e934936e9..b80b1b861cc 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -158,7 +158,7 @@ class @UsersSelect if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) selectedId = user.id - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e8c0172680d..18a74fe21a0 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -144,6 +144,10 @@ } } +.btn-lg { + padding: 12px 20px; +} + .btn-transparent { color: $btn-transparent-color; background-color: transparent; diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 0b3af592d4a..11f39d583bd 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -54,6 +54,10 @@ fill: #254e77 !important; } + .future { + visibility: hidden; + } + .domain-background { fill: none; shape-rendering: crispedges; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ba6c7930cdc..239eaf15cc1 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -248,7 +248,7 @@ .dropdown-title { position: relative; - padding: 0 25px 15px; + padding: 0 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -278,7 +278,7 @@ right: 5px; width: 20px; height: 20px; - top: -1px; + top: -3px; } .dropdown-menu-back { @@ -320,7 +320,7 @@ } } -.dropdown-input-field { +.dropdown-input-field, .default-dropdown-input { width: 100%; padding: 0 7px; color: $dropdown-input-color; @@ -358,6 +358,13 @@ border-top: 1px solid $dropdown-divider-color; } +.dropdown-due-date-footer { + padding-top: 0; + margin-left: 10px; + margin-right: 10px; + border-top: 0; +} + .dropdown-footer-list { font-size: 14px; @@ -395,3 +402,122 @@ height: 15px; border-radius: $border-radius-base; } + +.dropdown-menu-due-date { + .dropdown-content { + max-height: 230px; + } + + .ui-widget { + table { + margin: 0; + } + + &.ui-datepicker-inline { + padding: 0 10px; + border: 0; + width: 100%; + } + + .ui-datepicker-header { + padding: 0 8px 10px; + border: 0; + + .ui-icon { + background: none; + font-size: 20px; + text-indent: 0; + + &:before { + display: block; + position: relative; + top: -2px; + color: $dropdown-title-btn-color; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + } + } + + .ui-state-active, + .ui-state-hover { + color: $md-link-color; + background-color: $calendar-hover-bg; + } + + .ui-datepicker-prev, + .ui-datepicker-next { + top: 0; + height: 15px; + cursor: pointer; + + &:hover { + background-color: transparent; + border: 0; + + .ui-icon:before { + color: $md-link-color; + } + } + } + + .ui-datepicker-prev { + left: 0; + + .ui-icon:before { + content: '\f104'; + text-align: left; + } + } + + .ui-datepicker-next { + right: 0; + + .ui-icon:before { + content: '\f105'; + text-align: right; + } + } + + td { + padding: 0; + border: 1px solid $calendar-border-color; + + &:first-child { + border-left: 0; + } + + &:last-child { + border-right: 0; + } + + a { + line-height: 17px; + border: 0; + border-radius: 0; + } + } + + .ui-datepicker-title { + color: $gl-gray; + font-size: 15px; + line-height: 1; + font-weight: normal; + } + } + + th { + padding: 2px 0; + color: $calendar-header-color; + font-weight: normal; + text-transform: lowercase; + border-top: 1px solid $calendar-border-color; + } + + .ui-datepicker-unselectable { + background-color: $calendar-unselectable-bg; + } +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 789df42fb66..32cc15113f2 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -38,12 +38,14 @@ .filename { &.old { + display: inline-block; span.idiff { background-color: #f8cbcb; } } &.new { + display: inline-block; span.idiff { background-color: #a6f3a6; } @@ -129,6 +131,11 @@ td.line-numbers { float: none; border-left: 1px solid #ddd; + + i { + float: none; + margin-right: 0; + } } td.lines { padding: 0; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 0f32d36d59c..704fa1ff800 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -90,3 +90,12 @@ box-shadow: none; width: 100%; } + +.md { + &.md-preview-holder { + code { + white-space: pre-wrap; + word-break: break-all; + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f910cf61817..30ca27ab104 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5; $note-toolbar-color: #959494; $zen-control-hover-color: #111; + +$calendar-header-color: #b8b8b8; +$calendar-hover-bg: #ecf3fe; +$calendar-border-color: rgba(#000, .1); +$calendar-unselectable-bg: #faf9f9; diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss new file mode 100644 index 00000000000..125f495d6d4 --- /dev/null +++ b/app/assets/stylesheets/pages/confirmation.scss @@ -0,0 +1,18 @@ +.well-confirmation { + margin-bottom: 20px; + border-bottom: 1px solid #eee; + + > h1 { + font-weight: 400; + } + + .lead { + margin-bottom: 20px; + } +} + +.confirmation-content { + a { + color: $md-link-color; + } +} diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 2c2ac903f29..751a5ab4d92 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -36,4 +36,10 @@ } } } + + .wiki { + code { + white-space: pre-wrap; + } + } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 183f22a1b24..e7c8198ba45 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -34,6 +34,7 @@ background: #fff; color: #333; border-radius: 0 0 3px 3px; + -webkit-overflow-scrolling: auto; .unfold { cursor: pointer; @@ -86,7 +87,7 @@ } span { - white-space: pre; + white-space: pre-wrap; } } } @@ -335,7 +336,7 @@ } .diff-file .line_content { - white-space: pre; + white-space: pre-wrap; } .diff-wrap-lines .line_content { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 604f1700cf8..ee95bdf488e 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,8 +59,10 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { margin: -$gl-padding+1; + margin-top: 15px; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 5bf44c1cdb6..1cf3023ecc9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -128,6 +128,7 @@ top: 58px; bottom: 0; right: 0; + z-index: 10; transition: width .3s; background: $gray-light; padding: 10px 20px; @@ -241,16 +242,20 @@ } } - .btn { + .issuable-pager { background: $gray-normal; border: 1px solid $border-gray-normal; &:hover { background: $gray-dark; border: 1px solid $border-gray-dark; } + + &.btn-primary { + @extend .btn-primary + } } - a:not(.btn) { + a:not(.issuable-pager) { &:hover { color: $md-link-color; text-decoration: none; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 2f6d172568b..7fa13e66b43 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -84,18 +84,6 @@ border-color: $gl-success; } } - - p { - code { - white-space: normal; - } - - pre { - code { - white-space: pre; - } - } - } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ffcd89a3c9f..d54abe9bc02 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -81,16 +81,8 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - p { - code { - white-space: normal; - } - - pre { - code { - white-space: pre; - } - } + code { + white-space: pre-wrap; } // Reset ul style types since we're nested inside a ul already @@ -137,7 +129,7 @@ ul.notes { margin-right: 10px; } .line_content { - white-space: pre; + white-space: pre-wrap; } } @@ -171,11 +163,6 @@ ul.notes { &.parallel { border-width: 1px; - - .code, - code { - white-space: pre-wrap; - } } .notes { @@ -196,6 +183,9 @@ ul.notes { } } + .author_link { + color: $gl-gray; + } } .note-headline-light, @@ -308,7 +298,7 @@ ul.notes { padding: 4px; font-size: 16px; color: $gl-link-color; - margin-left: -60px; + margin-left: -56px; position: absolute; z-index: 10; width: 32px; diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb index f63b703d101..586f97c5eb4 100644 --- a/app/controllers/concerns/filter_projects.rb +++ b/app/controllers/concerns/filter_projects.rb @@ -10,6 +10,8 @@ module FilterProjects def filter_projects(projects) projects = projects.search(params[:filter_projects]) if params[:filter_projects].present? projects = projects.non_archived if params[:archived].blank? + projects = projects.personal(current_user) if params[:personal].present? && current_user + projects end end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index af1faca93f6..7b66ad3f92c 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -1,7 +1,16 @@ class ConfirmationsController < Devise::ConfirmationsController + def almost_there + flash[:notice] = nil + render layout: "devise_empty" + end + protected + def after_resending_confirmation_instructions_path_for(resource) + users_almost_there_path + end + def after_confirmation_path_for(resource_name, resource) if signed_in?(resource_name) after_sign_in_path_for(resource) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 74150ad606b..be872a93fee 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -83,8 +83,7 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - view = params[:view] || cookies[:diff_view] - cookies.permanent[:diff_view] = params[:view] = view if view + cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? end def builds_enabled diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 4d64a2d9884..a202cb38692 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -38,13 +38,13 @@ class Projects::CommitController < Projects::ApplicationController end def cancel_builds - ci_commit.builds.running_or_pending.each(&:cancel) + ci_builds.running_or_pending.each(&:cancel) redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end def retry_builds - ci_commit.builds.latest.failed.each do |build| + ci_builds.latest.failed.each do |build| if build.retryable? Ci::Build.retry(build) end @@ -99,8 +99,12 @@ class Projects::CommitController < Projects::ApplicationController @commit ||= @project.commit(params[:id]) end - def ci_commit - @ci_commit ||= project.ci_commit(commit.sha) + def ci_commits + @ci_commits ||= project.ci_commits.where(sha: commit.sha) + end + + def ci_builds + @ci_builds ||= Ci::Build.where(commit: ci_commits) end def define_show_vars @@ -113,7 +117,8 @@ class Projects::CommitController < Projects::ApplicationController @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count - @statuses = ci_commit.statuses if ci_commit + @statuses = CommitStatus.where(commit: ci_commits) + @builds = Ci::Build.where(commit: ci_commits) end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 85d5b52fa05..7d4fc361ce2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController end @issues = @issues.page(params[:page]) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.atom { render layout: false } format.json do render json: { - html: view_to_html_string("projects/issues/_issues") + html: view_to_html_string("projects/issues/_issues"), + labels: @labels.as_json(methods: :text_color) } end end @@ -100,7 +101,6 @@ class Projects::IssuesController < Projects::ApplicationController end respond_to do |format| - format.js format.html do if @issue.valid? redirect_to issue_path(@issue) @@ -109,7 +109,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) + render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) end end end @@ -191,7 +191,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue_params params.require(:issue).permit( :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :state_event, :task_num, label_ids: [] + :milestone_id, :due_date, :state_event, :task_num, label_ids: [] ) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3e0cfc6aa65..9c147b3689e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.json do render json: { - html: view_to_html_string("projects/merge_requests/_merge_requests") + html: view_to_html_string("projects/merge_requests/_merge_requests"), + labels: @labels.as_json(methods: :text_color) } end end @@ -148,13 +149,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController if @merge_request.valid? respond_to do |format| - format.js format.html do redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) end format.json do - render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) + render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) end end else @@ -320,6 +320,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_widget_vars @ci_commit = @merge_request.ci_commit + @ci_commits = [@ci_commit].compact closes_issues end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e457db2f0b7..33b2625c0ac 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,6 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize - before_action :authorize_admin_project_member!, except: :leave + before_action :authorize_admin_project_member!, except: [:leave, :index] def index @project_members = @project.project_members diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c48175a4c5a..059b88e2253 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -31,11 +31,11 @@ class RegistrationsController < Devise::RegistrationsController end def after_sign_up_path_for(_resource) - new_user_session_path + users_almost_there_path end def after_inactive_sign_up_path_for(_resource) - new_user_session_path + users_almost_there_path end private diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f1df6832bf6..f00f3f709e9 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -39,6 +39,7 @@ class IssuableFinder items = by_assignee(items) items = by_author(items) items = by_label(items) + items = by_due_date(items) sort(items) end @@ -117,7 +118,7 @@ class IssuableFinder end def filter_by_no_label? - labels? && params[:label_name] == Label::None.title + labels? && params[:label_name].include?(Label::None.title) end def labels @@ -271,7 +272,6 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names) - if projects items = items.where(labels: { project_id: projects }) end @@ -281,8 +281,44 @@ class IssuableFinder items end + def by_due_date(items) + if due_date? + if filter_by_no_due_date? + items = items.without_due_date + elsif filter_by_overdue? + items = items.due_before(Date.today) + elsif filter_by_due_this_week? + items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) + elsif filter_by_due_this_month? + items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) + end + end + + items + end + + def filter_by_no_due_date? + due_date? && params[:due_date] == Issue::NoDueDate.name + end + + def filter_by_overdue? + due_date? && params[:due_date] == Issue::Overdue.name + end + + def filter_by_due_this_week? + due_date? && params[:due_date] == Issue::DueThisWeek.name + end + + def filter_by_due_this_month? + due_date? && params[:due_date] == Issue::DueThisMonth.name + end + + def due_date? + params[:due_date].present? && klass.column_names.include?('due_date') + end + def label_names - params[:label_name].split(',') + params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] end def current_user_related? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 16e5b8ac223..3e0074da394 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -254,11 +254,11 @@ module ApplicationHelper def page_filter_path(options = {}) without = options.delete(:without) + add_label = options.delete(:label) exist_opts = { state: params[:state], scope: params[:scope], - label_name: params[:label_name], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], @@ -275,6 +275,13 @@ module ApplicationHelper path = request.path path << "?#{options.to_param}" + if add_label + if params[:label_name].present? and params[:label_name].respond_to?('any?') + params[:label_name].each do |label| + path << "&label_name[]=#{label}" + end + end + end path end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8b1575d5e0c..417050b4132 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -4,14 +4,6 @@ module CiStatusHelper builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end - def ci_status_icon(ci_commit) - ci_icon_for_status(ci_commit.status) - end - - def ci_status_label(ci_commit) - ci_label_for_status(ci_commit.status) - end - def ci_status_with_icon(status, target = nil) content = ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) klass = "ci-status ci-#{status}" @@ -47,10 +39,13 @@ module CiStatusHelper end def render_ci_status(ci_commit, tooltip_placement: 'auto left') - link_to ci_status_icon(ci_commit), + # TODO: split this method into + # - render_commit_status + # - render_pipeline_status + link_to ci_icon_for_status(ci_commit.status), ci_status_path(ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", - title: "Build #{ci_status_label(ci_commit)}", + title: "Build #{ci_label_for_status(ci_commit.status)}", data: { toggle: 'tooltip', placement: tooltip_placement } end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 6a3ec83b8c0..97466d532f4 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -9,7 +9,13 @@ module DiffHelper end def diff_view - params[:view] == 'parallel' ? 'parallel' : 'inline' + diff_views = %w(inline parallel) + + if diff_views.include?(cookies[:diff_view]) + cookies[:diff_view] + else + diff_views.first + end end def diff_hard_limit_enabled? diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f3fddef01cb..f07eff3fb57 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -25,6 +25,10 @@ module GitlabRoutingHelper namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) end + def project_pipelines_path(project, *args) + namespace_project_pipelines_path(project.namespace, project, *args) + end + def project_builds_path(project, *args) namespace_project_builds_path(project.namespace, project, *args) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 49b6b79ce35..39474217286 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,25 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def multi_label_name(current_labels, default_label) + # current_labels may be a string from before + if current_labels.is_a?(Array) + if current_labels.count > 1 + "#{current_labels[0]} +#{current_labels.count - 1} more" + else + current_labels[0] + end + elsif current_labels.is_a?(String) + if current_labels.nil? || current_labels.empty? + default_label + else + current_labels + end + else + default_label + end + end + def issuable_json_path(issuable) project = issuable.project diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 4cb8adcebad..afe1e11a0da 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -131,7 +131,7 @@ module IssuesHelper class: "icon emoji-icon emoji-#{unicode}", title: name, data: data - else + else # Emoji icons displayed separately, used for the awards already given # to an issue or merge request. content_tag :img, "", @@ -172,6 +172,18 @@ module IssuesHelper end.to_h end + def due_date_options + options = [ + Issue::AnyDueDate, + Issue::NoDueDate, + Issue::DueThisWeek, + Issue::DueThisMonth, + Issue::Overdue + ] + + options_from_collection_for_select(options, 'name', 'title', params[:due_date]) + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 82f805fa444..e4e8b934bc8 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -84,6 +84,14 @@ module PageLayoutHelper end end + def nav(name = nil) + if name + @nav = name + else + @nav + end + end + def fluid_layout(enabled = false) if @fluid_layout.nil? @fluid_layout = (current_user && current_user.layout == "fluid") || enabled diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 2f164da326c..3d5e61d2c18 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -123,6 +123,18 @@ module ProjectsHelper end end + def license_short_name(project) + no_license_key = project.repository.license_key.nil? || + # Back-compat if cache contains 'no-license', can be removed in a few weeks + project.repository.license_key == 'no-license' + + return 'LICENSE' if no_license_key + + license = Licensee::License.new(project.repository.license_key) + + license.nickname || license.name + end + private def get_project_nav_tabs(project, current_user) @@ -144,6 +156,10 @@ module ProjectsHelper nav_tabs << :settings end + if can?(current_user, :read_project_member, project) + nav_tabs << :team + end + if can?(current_user, :read_issue, project) nav_tabs << :issues end @@ -316,14 +332,6 @@ module ProjectsHelper @ref || @repository.try(:root_ref) end - def license_short_name(project) - license = Licensee::License.new(project.repository.license_key) - - license.nickname || license.name - end - - private - def filename_path(project, filename) if project && blob = project.repository.send(filename) namespace_project_blob_path( diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 2f2d2721d6d..630e10ea892 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -8,6 +8,8 @@ module SortingHelper sort_value_oldest_created => sort_title_oldest_created, sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_later => sort_title_milestone_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_due_date_later => sort_title_due_date_later, sort_value_largest_repo => sort_title_largest_repo, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, @@ -50,6 +52,14 @@ module SortingHelper 'Milestone due later' end + def sort_title_due_date_soon + 'Due soon' + end + + def sort_title_due_date_later + 'Due later' + end + def sort_title_name 'Name' end @@ -98,6 +108,14 @@ module SortingHelper 'milestone_due_desc' end + def sort_value_due_date_soon + 'due_date_asc' + end + + def sort_value_due_date_later + 'due_date_desc' + end + def sort_value_name 'name_asc' end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 04e53fe7c61..96a83671009 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -110,4 +110,12 @@ module TabHelper 'active' end end + + def profile_tab_class + if controller.controller_path =~ /\Aprofiles/ + return 'active' + end + + 'active' if current_controller?('oauth/applications') + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d42a65620ff..553cd447971 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -37,8 +37,6 @@ module Ci class Build < CommitStatus - LAZY_ATTRIBUTES = ['trace'] - belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' belongs_to :erased_by, class_name: 'User' @@ -50,25 +48,17 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } - scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader acts_as_taggable - # To prevent db load megabytes of data from trace - default_scope -> { select(Ci::Build.columns_without_lazy) } - before_destroy { project } - class << self - def columns_without_lazy - (column_names - LAZY_ATTRIBUTES).map do |column_name| - "#{table_name}.#{column_name}" - end - end + after_create :execute_hooks + class << self def last_month where('created_at > ?', Date.today - 1.month) end @@ -126,12 +116,16 @@ module Ci end def retried? - !self.commit.latest_statuses_for_ref(self.ref).include?(self) + !self.commit.statuses.latest.include?(self) + end + + def retry + Ci::Build.retry(self) end def depends_on_builds # Get builds of the same type - latest_builds = self.commit.builds.similar(self).latest + latest_builds = self.commit.builds.latest # Return builds from previous stages latest_builds.where('stage_idx < ?', stage_idx) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index f4cf7034b14..f2667e5476b 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -19,21 +19,28 @@ module Ci class Commit < ActiveRecord::Base extend Ci::Model + include Statuseable belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :statuses, class_name: 'CommitStatus' has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + delegate :stages, to: :statuses + validates_presence_of :sha + validates_presence_of :status validate :valid_commit_sha + # Invalidate object and save if when touched + after_touch :update_state + def self.truncate_sha(sha) sha[0...8] end - def to_param - sha + def self.stages + CommitStatus.where(commit: all).stages end def project_id @@ -68,15 +75,20 @@ module Ci nil end - def stage - running_or_pending = statuses.latest.running_or_pending.ordered - running_or_pending.first.try(:stage) + def branch? + !tag? + end + + def retryable? + builds.latest.any? do |build| + build.failed? && build.retryable? + end end - def create_builds(ref, tag, user, trigger_request = nil) + def create_builds(user, trigger_request = nil) return unless config_processor config_processor.stages.any? do |stage| - CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present? + CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present? end end @@ -84,7 +96,7 @@ module Ci return unless config_processor # don't create other builds if this one is retried - latest_builds = builds.similar(build).latest + latest_builds = builds.latest return unless latest_builds.exists?(build.id) # get list of stages after this build @@ -92,88 +104,21 @@ module Ci next_stages.delete(build.stage) # get status for all prior builds - prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) } - status = Ci::Status.get_status(prior_builds) + prior_builds = latest_builds.where.not(stage: next_stages) + prior_status = prior_builds.status # create builds for next stages based next_stages.any? do |stage| - CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present? + CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present? end end - def refs - statuses.order(:ref).pluck(:ref).uniq - end - - def latest_statuses - @latest_statuses ||= statuses.latest.to_a - end - - def latest_statuses_for_ref(ref) - latest_statuses.select { |status| status.ref == ref } - end - - def matrix_builds(build = nil) - matrix_builds = builds.latest.ordered - matrix_builds = matrix_builds.similar(build) if build - matrix_builds.to_a - end - def retried @retried ||= (statuses.order(id: :desc) - statuses.latest) end - def status - if yaml_errors.present? - return 'failed' - end - - @status ||= Ci::Status.get_status(latest_statuses) - end - - def pending? - status == 'pending' - end - - def running? - status == 'running' - end - - def success? - status == 'success' - end - - def failed? - status == 'failed' - end - - def canceled? - status == 'canceled' - end - - def active? - running? || pending? - end - - def complete? - canceled? || success? || failed? - end - - def duration - duration_array = statuses.map(&:duration).compact - duration_array.reduce(:+).to_i - end - - def started_at - @started_at ||= statuses.order('started_at ASC').first.try(:started_at) - end - - def finished_at - @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at) - end - def coverage - coverage_array = latest_statuses.map(&:coverage).compact + coverage_array = statuses.latest.map(&:coverage).compact if coverage_array.size >= 1 '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end @@ -181,23 +126,29 @@ module Ci def config_processor return nil unless ci_yaml_file - @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) - rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e - save_yaml_error(e.message) - nil - rescue - save_yaml_error("Undefined error") - nil + return @config_processor if defined?(@config_processor) + + @config_processor ||= begin + Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) + rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e + save_yaml_error(e.message) + nil + rescue + save_yaml_error("Undefined error") + nil + end end def ci_yaml_file + return @ci_yaml_file if defined?(@ci_yaml_file) + @ci_yaml_file ||= begin blob = project.repository.blob_at(sha, '.gitlab-ci.yml') blob.load_all_data!(project.repository) blob.data + rescue + nil end - rescue - nil end def skip_ci? @@ -206,10 +157,23 @@ module Ci private + def update_state + statuses.reload + self.status = if yaml_errors.blank? + statuses.latest.status || 'skipped' + else + 'failed' + end + self.started_at = statuses.started_at + self.finished_at = statuses.finished_at + self.duration = statuses.latest.duration + save + end + def save_yaml_error(error) return if self.yaml_errors? self.yaml_errors = error - save + update_state end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 6bb018b086f..562c3ed15b2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -207,12 +207,13 @@ class Commit @raw.short_id(7) end - def ci_commit - project.ci_commit(sha) + def ci_commits + @ci_commits ||= project.ci_commits.where(sha: sha) end def status - ci_commit.try(:status) || :not_found + return @status if defined?(@status) + @status ||= ci_commits.status end def revert_branch_name diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3377a85a55a..aa56314aa16 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -33,30 +33,23 @@ # class CommitStatus < ActiveRecord::Base + include Statuseable + self.table_name = 'ci_builds' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :commit, class_name: 'Ci::Commit', touch: true belongs_to :user validates :commit, presence: true - validates :status, inclusion: { in: %w(pending running failed success canceled) } validates_presence_of :name alias_attribute :author, :user - scope :running, -> { where(status: 'running') } - scope :pending, -> { where(status: 'pending') } - scope :success, -> { where(status: 'success') } - scope :failed, -> { where(status: 'failed') } - scope :running_or_pending, -> { where(status: [:running, :pending]) } - scope :finished, -> { where(status: [:success, :failed, :canceled]) } - scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } + scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } - scope :for_ref, ->(ref) { where(ref: ref) } - - AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled'] + scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status, initial: :pending do event :run do @@ -86,31 +79,24 @@ class CommitStatus < ActiveRecord::Base after_transition [:pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) end - - state :pending, value: 'pending' - state :running, value: 'running' - state :failed, value: 'failed' - state :success, value: 'success' - state :canceled, value: 'canceled' end - delegate :sha, :short_sha, to: :commit, prefix: false + delegate :sha, :short_sha, to: :commit - # TODO: this should be removed with all references def before_sha - Gitlab::Git::BLANK_SHA + commit.before_sha || Gitlab::Git::BLANK_SHA end - def started? - !pending? && !canceled? && started_at + def self.stages + order_by = 'max(stage_idx)' + group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact end - def active? - running? || pending? - end - - def complete? - canceled? || success? || failed? + def self.stages_status + all.stages.inject({}) do |h, stage| + h[stage] = all.where(stage: stage).status + h + end end def ignored? @@ -118,11 +104,13 @@ class CommitStatus < ActiveRecord::Base end def duration - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end + duration = + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + duration end def stuck? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index afa2ca039ae..d5166e81474 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -37,7 +37,6 @@ module Issuable scope :closed, -> { with_state(:closed) } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } - scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } @@ -122,6 +121,14 @@ module Issuable joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") end + + def with_label(title) + if title.is_a?(Array) && title.count > 1 + joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}") + else + joins(:labels).where(labels: { title: title }) + end + end end def today? diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb new file mode 100644 index 00000000000..8a293b7b76e --- /dev/null +++ b/app/models/concerns/statuseable.rb @@ -0,0 +1,81 @@ +module Statuseable + extend ActiveSupport::Concern + + AVAILABLE_STATUSES = %w(pending running success failed canceled skipped) + + class_methods do + def status_sql + builds = all.select('count(*)').to_sql + success = all.success.select('count(*)').to_sql + ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored) + ignored ||= '0' + pending = all.pending.select('count(*)').to_sql + running = all.running.select('count(*)').to_sql + canceled = all.canceled.select('count(*)').to_sql + skipped = all.skipped.select('count(*)').to_sql + + deduce_status = "(CASE + WHEN (#{builds})=0 THEN NULL + WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success' + WHEN (#{builds})=(#{pending}) THEN 'pending' + WHEN (#{builds})=(#{canceled}) THEN 'canceled' + WHEN (#{builds})=(#{skipped}) THEN 'skipped' + WHEN (#{running})+(#{pending})>0 THEN 'running' + ELSE 'failed' + END)" + + deduce_status + end + + def status + all.pluck(self.status_sql).first + end + + def duration + duration_array = all.map(&:duration).compact + duration_array.reduce(:+) + end + + def started_at + all.minimum(:started_at) + end + + def finished_at + all.maximum(:finished_at) + end + end + + included do + validates :status, inclusion: { in: AVAILABLE_STATUSES } + + state_machine :status, initial: :pending do + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + state :skipped, value: 'skipped' + end + + scope :running, -> { where(status: 'running') } + scope :pending, -> { where(status: 'pending') } + scope :success, -> { where(status: 'success') } + scope :failed, -> { where(status: 'failed') } + scope :canceled, -> { where(status: 'canceled') } + scope :skipped, -> { where(status: 'skipped') } + scope :running_or_pending, -> { where(status: [:running, :pending]) } + scope :finished, -> { where(status: [:success, :failed, :canceled]) } + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index a009e235b37..ea1bfb776ee 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -28,6 +28,13 @@ class Issue < ActiveRecord::Base include Sortable include Taskable + DueDateStruct = Struct.new(:title, :name).freeze + NoDueDate = DueDateStruct.new('No Due Date', '0').freeze + AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze + Overdue = DueDateStruct.new('Overdue', 'overdue').freeze + DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze + DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + ActsAsTaggableOn.strict_case_match = true belongs_to :project @@ -39,6 +46,13 @@ class Issue < ActiveRecord::Base scope :open_for, ->(user) { opened.assigned_to(user) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) } + scope :without_due_date, -> { where(due_date: nil) } + scope :due_before, ->(date) { where('issues.due_date < ?', date) } + scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } + + scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } + scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -82,6 +96,15 @@ class Issue < ActiveRecord::Base @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end + def self.sort(method) + case method.to_s + when 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + else + super + end + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -169,4 +192,8 @@ class Issue < ActiveRecord::Base self.related_branches(current_user).empty? && self.closed_by_merge_requests(current_user).empty? end + + def overdue? + due_date.try(:past?) || false + end end diff --git a/app/models/label.rb b/app/models/label.rb index 55c01cae762..60bdce32952 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -113,6 +113,10 @@ class Label < ActiveRecord::Base template end + def text_color + LabelsHelper::text_color_for_bg(self.color) + end + private def label_format_reference(format = :id) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dbecc48485c..d00919c3b0c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -586,7 +586,7 @@ class MergeRequest < ActiveRecord::Base end def ci_commit - @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project + @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project end def diff_refs diff --git a/app/models/project.rb b/app/models/project.rb index 8f0272d2ce0..7aa21b19e67 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -963,12 +963,12 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def ci_commit(sha) - ci_commits.find_by(sha: sha) + def ci_commit(sha, ref) + ci_commits.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_ci_commit(sha) - ci_commit(sha) || ci_commits.create(sha: sha) + def ensure_ci_commit(sha, ref) + ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref) end def enable_ci diff --git a/app/models/repository.rb b/app/models/repository.rb index da751591103..61c8dce6060 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -466,8 +466,8 @@ class Repository return nil if !exists? || empty? cache.fetch(:license_blob) do - if licensee_project.license - blob_at_branch(root_ref, licensee_project.matched_file.filename) + tree(:head).blobs.find do |file| + file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i end end end @@ -476,7 +476,7 @@ class Repository return nil if !exists? || empty? cache.fetch(:license_key) do - licensee_project.license.try(:key) || 'no-license' + Licensee.license(path).try(:key) end end @@ -959,8 +959,4 @@ class Repository def cache @cache ||= RepositoryCache.new(path_with_namespace) end - - def licensee_project - @licensee_project ||= Licensee.project(path) - end end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 2cd51a7610f..18274ce24e2 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,7 +1,11 @@ module Ci class CreateBuildsService - def execute(commit, stage, ref, tag, user, trigger_request, status) - builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request) + def initialize(commit) + @commit = commit + end + + def execute(stage, user, status, trigger_request = nil) + builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request) # check when to create next build builds_attrs = builds_attrs.select do |build_attrs| @@ -17,7 +21,8 @@ module Ci builds_attrs.map do |build_attrs| # don't create the same build twice - unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name]) + unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag, + trigger_request: trigger_request, name: build_attrs[:name]) build_attrs.slice!(:name, :commands, :tag_list, @@ -26,17 +31,21 @@ module Ci :stage, :stage_idx) - build_attrs.merge!(ref: ref, - tag: tag, + build_attrs.merge!(ref: @commit.ref, + tag: @commit.tag, trigger_request: trigger_request, user: user, - project: commit.project) + project: @commit.project) - build = commit.builds.create!(build_attrs) - build.execute_hooks - build + @commit.builds.create!(build_attrs) end end end + + private + + def config_processor + @config_processor ||= @commit.config_processor + end end end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index b3dfc707221..993acf11db9 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -7,14 +7,14 @@ module Ci # check if ref is tag tag = project.repository.find_tag(ref).present? - ci_commit = project.ensure_ci_commit(commit.sha) + ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag) trigger_request = trigger.trigger_requests.create!( variables: variables, commit: ci_commit, ) - if ci_commit.create_builds(ref, tag, nil, trigger_request) + if ci_commit.create_builds(nil, trigger_request) trigger_request end end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 50c95ced8a7..3018f27ec05 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -3,8 +3,9 @@ module Ci def execute(project, opts) sha = opts[:sha] || ref_sha(project, opts[:ref]) - commit = project.ci_commits.find_by(sha: sha) - image_name = image_for_commit(commit) + ci_commits = project.ci_commits.where(sha: sha) + ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref] + image_name = image_for_status(ci_commits.status) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) @@ -16,9 +17,9 @@ module Ci project.commit(ref).try(:sha) if ref end - def image_for_commit(commit) - return 'build-unknown.svg' unless commit - 'build-' + commit.status + ".svg" + def image_for_status(status) + status ||= 'unknown' + 'build-' + status + ".svg" end end end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 69d5c42a877..0d2aa1ff03d 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -2,6 +2,7 @@ class CreateCommitBuildsService def execute(project, user, params) return false unless project.builds_enabled? + before_sha = params[:checkout_sha] || params[:before] sha = params[:checkout_sha] || params[:after] origin_ref = params[:ref] @@ -10,15 +11,16 @@ class CreateCommitBuildsService end ref = Gitlab::Git.ref_name(origin_ref) + tag = Gitlab::Git.tag_ref?(origin_ref) # Skip branch removal if sha == Gitlab::Git::BLANK_SHA return false end - commit = project.ci_commit(sha) + commit = project.ci_commit(sha, ref) unless commit - commit = project.ci_commits.new(sha: sha) + commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag) # Skip creating ci_commit when no gitlab-ci.yml is found unless commit.ci_yaml_file @@ -32,10 +34,10 @@ class CreateCommitBuildsService # Skip creating builds for commits that have [ci skip] unless commit.skip_ci? # Create builds for commit - tag = Gitlab::Git.tag_ref?(origin_ref) - commit.create_builds(ref, tag, user) + commit.create_builds(user) end + commit.touch commit end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 18f76d3f650..2b16089df1b 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -37,8 +37,9 @@ class IssuableBaseService < BaseService end def filter_params(issuable_ability_name = :issue) - params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE - params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + filter_assignee + filter_milestone + filter_labels ability = :"admin_#{issuable_ability_name}" @@ -49,6 +50,29 @@ class IssuableBaseService < BaseService end end + def filter_assignee + if params[:assignee_id] == IssuableFinder::NONE + params[:assignee_id] = '' + end + end + + def filter_milestone + milestone_id = params[:milestone_id] + return unless milestone_id + + if milestone_id == IssuableFinder::NONE || + project.milestones.find_by(id: milestone_id).nil? + params[:milestone_id] = '' + end + end + + def filter_labels + return if params[:label_ids].to_a.empty? + + params[:label_ids] = + project.labels.where(id: params[:label_ids]).pluck(:id) + end + def update(issuable) change_state(issuable) filter_params diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index b05fdbd5552..fe0b9d3a491 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -7,17 +7,17 @@ .form-group = f.label :name, class: 'control-label' .col-sm-10 - = f.text_field :name, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group = f.label :username, class: 'control-label' .col-sm-10 - = f.text_field :username, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group = f.label :email, class: 'control-label' .col-sm-10 - = f.text_field :email, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required - if @user.new_record? diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f9ec3a89158..49ab8aad1d5 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -45,6 +45,7 @@ .prepend-top-default - if @todos.any? + .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| .panel.panel-default.panel-small.js-todos-list - project = group[0] diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml new file mode 100644 index 00000000000..3c3830a3f10 --- /dev/null +++ b/app/views/devise/confirmations/almost_there.haml @@ -0,0 +1,10 @@ +.well-confirmation.text-center + %h1.prepend-top-0 + Almost there... + %p.lead + Please check your email to confirm your account +%p.confirmation-content.text-center + No confirmation email received? Please check your spam folder or +.append-bottom-20.prepend-top-20.text-center + %a.btn.btn-lg.btn-success{ href: new_user_confirmation_path } + Request new confirmation email diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 0aff79749ef..79df17ba612 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -1,5 +1,4 @@ - page_title "Applications" -- header_title page_title, applications_profile_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588d..ca9c2a0bf2e 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,6 +25,10 @@ .content-wrapper = render "layouts/flash" = yield :flash_message + - if defined?(nav) && nav + .layout-nav + %div{ class: container_class } + = render "layouts/nav/#{nav}" %div{ class: (container_class unless @no_container) } .content .clearfix diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index babfb032236..e4d1c773d03 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,6 +6,6 @@ = yield :scripts_body_top = render "layouts/header/default", title: header_title - = render 'layouts/page', sidebar: sidebar + = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml new file mode 100644 index 00000000000..7c061dd531f --- /dev/null +++ b/app/views/layouts/devise_empty.html.haml @@ -0,0 +1,17 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head" + %body.ui_charcoal.login-page.application.navless + = render "layouts/header/empty" + = render "layouts/broadcast" + .container.navless-container + .content + = render "layouts/flash" + = yield + + %hr + .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/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5cef652da14..ca49c313ff7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -48,8 +48,7 @@ %span Help - %li.separate-item - = nav_link(controller: :profile) do + = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc5..d730840d63a 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,17 +1,9 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - +%ul.nav-links = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') %span - Profile Settings + Profile = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account' do = icon('gear fw') @@ -27,7 +19,6 @@ = icon('envelope-o fw') %span Emails - %span.count= number_with_delimiter(current_user.emails.count + 1) - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to edit_profile_password_path, title: 'Password' do @@ -45,7 +36,6 @@ = icon('key fw') %span SSH Keys - %span.count= number_with_delimiter(current_user.keys.count) = nav_link(controller: :preferences) do = link_to profile_preferences_path, title: 'Preferences' do -# TODO (rspeicher): Better icon? diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75e..a15b7758c4b 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -77,7 +77,7 @@ Merge Requests %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :settings + - if project_nav_tab? :team = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do = icon('users fw') diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index dfa6cc5702e..b77d3402a2e 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,6 @@ - page_title "Profile Settings" - header_title "Profile Settings", profile_path unless header_title -- sidebar "profile" +- sidebar "dashboard" +- nav "profile" = render template: "layouts/application" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 6efd119f260..afd3d79321f 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,5 +1,4 @@ - page_title "Account" -- header_title page_title, profile_account_path - if current_user.ldap_user? .alert.alert-info diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index f630c03e5f6..9c404b6935f 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,5 +1,4 @@ - page_title "Audit Log" -- header_title page_title, audit_log_profile_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 3f328f96cea..57527361eb6 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,5 +1,4 @@ - page_title "Emails" -- header_title page_title, profile_emails_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index e0f8c9a5733..6a067a03535 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,5 +1,4 @@ - page_title "SSH Keys" -- header_title page_title, profile_keys_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index a2a505c082b..7696f112bb3 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,5 +1,4 @@ - page_title "Notifications" -- header_title page_title, profile_notifications_path %div - if @user.errors.any? diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 5ac8a8b9d09..243428b690e 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Password" -- header_title page_title, edit_profile_password_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index f80211669fb..bfe53be6854 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,5 +1,4 @@ - page_title 'Preferences' -- header_title page_title, profile_preferences_path = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f| .col-lg-3.profile-settings-sidebar diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 961b61d2e76..48b0dd6b121 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -9,4 +9,7 @@ = spinner :javascript - new Activities(); + var activity = new Activities(); + $(document).on('page:restore', function (event) { + activity.reloadActivities() + }) diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index b6074373e2b..0de019983ca 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -55,6 +55,9 @@ %li gcovr (C/C++) - %code ^TOTAL.*\s+(\d+\%)$ + %li + tap --coverage-report=text-summary (Node.js) - + %code ^Statements\s*:\s*([^%]+) .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 386d72e7787..66c30283c7a 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,9 +1,8 @@ .project-last-commit - - ci_commit = project.ci_commit(commit.sha) - - if ci_commit - = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do - = ci_status_icon(ci_commit) - = ci_status_label(ci_commit) + - if commit.status + = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do + = ci_icon_for_status(commit.status) + = ci_label_for_status(commit.status) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 38e62c81fed..5926d181ba3 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -1,17 +1,17 @@ - if @lines.present? - if @form.unfold? && @form.since != 1 && !@form.bottom? - %tr.line_holder{ id: @form.since } + %tr.line_holder = render "projects/diffs/match_line", { line: @match_line, line_old: @form.since, line_new: @form.since, bottom: false, new_file: false } - @lines.each_with_index do |line, index| - line_new = index + @form.since - line_old = line_new - @form.offset - %tr.line_holder + %tr.line_holder{ id: line_old } %td.old_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_old), "#" + = link_to raw(line_old), "##{line_old}" %td.new_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_new) , "#" + = link_to raw(line_new) , "##{line_old}" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index aa85f495e39..0406fc21d77 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -58,6 +58,6 @@ %th Coverage %th - = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? + = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 6d4505ebb60..99d72aa7935 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -13,7 +13,7 @@ = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) #up-build-trace - - builds = @build.commit.matrix_builds(@build) + - builds = @build.commit.builds.latest.to_a - if builds.size > 1 %ul.nav-links.no-top.no-bottom - builds.each do |build| diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 2cf9115e4dd..e123eb1cc7a 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -19,11 +19,12 @@ %td = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" - %td - - if build.ref - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) - - else - .light none + - if defined?(ref) && ref + %td + - if build.ref + = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) + - else + .light none - if defined?(runner) && runner %td @@ -48,6 +49,8 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail + - if defined?(retried) && retried + %span.label.label-warning retried %td.duration - if build.duration diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 003b7c18d0e..5c9a319edeb 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,67 +1,2 @@ -.gray-content-block.middle-block - .pull-right - - if can?(current_user, :update_build, @ci_commit.project) - - if @ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post - - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - - .oneline - = pluralize @statuses.count(:id), "build" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace" - - if @ci_commit.duration > 0 - in - = time_interval_in_words @ci_commit.duration - -- if @ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - @ci_commit.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - -- if @ci_commit.project.builds_enabled? && !@ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_commit.project.build_coverage_enabled? - %th Coverage - %th - - @ci_commit.refs.each do |ref| - - builds = @ci_commit.statuses.for_ref(ref).latest.ordered - = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true - -- if @ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_commit.project.build_coverage_enabled? - %th Coverage - %th - = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true +- @ci_commits.each do |ci_commit| + = render "ci_commit", ci_commit: ci_commit diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml new file mode 100644 index 00000000000..25714e6cb47 --- /dev/null +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -0,0 +1,69 @@ +.gray-content-block.middle-block + .pull-right + - if can?(current_user, :update_build, @project) + - if ci_commit.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + + - if ci_commit.builds.running_or_pending.any? + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline + = pluralize ci_commit.statuses.count(:id), "build" + - if ci_commit.ref + for + %span.label.label-info + = ci_commit.ref + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" + - if ci_commit.duration > 0 + in + = time_interval_in_words ci_commit.duration + +- if ci_commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - ci_commit.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + +- if @project.builds_enabled? && !ci_commit.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th + - builds = ci_commit.statuses.latest.ordered + = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true + +- if ci_commit.retried.any? + .gray-content-block.second-block + Retried builds + + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th + = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index d6c9e54e657..3d7c18a5f58 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -43,12 +43,12 @@ - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" -- if @ci_commit +- if @commit.status .pull-right - = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do - = ci_status_icon(@ci_commit) + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do + = ci_icon_for_status(@commit.status) build: - = ci_status_label(@ci_commit) + = ci_label_for_status(@commit.status) .commit-info-row.branches %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index e550af7888a..e5e3d696035 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -5,7 +5,7 @@ .prepend-top-default = render "commit_box" -- if @ci_commit +- if @commit.status = render "ci_menu" - else %div.block-connector diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index d71f61466f1..c7d8c9a0d15 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -4,9 +4,8 @@ - notes = commit.notes - note_count = notes.user.count -- ci_commit = project.ci_commit(commit.sha) - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] -- cache_key.push(ci_commit.status) if ci_commit +- cache_key.push(commit.status) if commit.status = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } @@ -17,8 +16,8 @@ %a.text-expander.js-toggle-button ... .pull-right - - if ci_commit - = render_ci_status(ci_commit) + - if commit.status + = render_ci_status(commit) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index c52cf25d40a..bcdb09208aa 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -39,4 +39,4 @@ = spinner :javascript - CommitsList.init("#{@ref}", #{@limit}); + CommitsList.init(#{@limit}); diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index c15386b4883..f21c864e35c 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -15,12 +15,13 @@ - if defined?(commit_sha) && commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" - - %td - - if generic_commit_status.ref - = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) - - else - .light none + + - if defined?(ref) && ref + %td + - if generic_commit_status.ref + = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) + - else + .light none - if defined?(runner) && runner %td diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 7a8009f6da4..9ad86ed71c9 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -48,6 +48,11 @@ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = icon('clock-o') = issue.milestone.title + - if issue.due_date + %span{class: "#{'cred' if issue.overdue?}"} + + = icon('calendar') + = issue.due_date.to_s(:medium) - if issue.labels.any? - issue.labels.each do |label| diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index b10cd03515f..bdfa0c7009e 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,7 +5,7 @@ - @related_branches.each do |branch| %li - sha = @project.repository.find_branch(branch).target - - ci_commit = @project.ci_commit(sha) if sha + - ci_commit = @project.ci_commit(sha, branch) if sha - if ci_commit %span.related-branch-ci-status = render_ci_status(ci_commit) diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml deleted file mode 100644 index e69de29bb2d..00000000000 --- a/app/views/projects/issues/update.js.haml +++ /dev/null diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 8d05060f563..290753d57c6 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,7 +3,7 @@ - page_card_attributes @merge_request.card_attributes - header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) -- if params[:view] == 'parallel' +- if diff_view == 'parallel' - fluid_layout true .merge-request{'data-url' => merge_request_path(@merge_request)} diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index 8ac653427c9..f5bf16ef3ad 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.title} (#{merge_request.to_reference}", "Merge Requests" +- page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" = render "header_title" .merge-request diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index 307a75d02ca..a116ffe2e15 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1 +1,2 @@ -= render "projects/commit/builds", link_to_commit: true += render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true + diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 0a99e8c9591..36c275e8be1 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -24,9 +24,9 @@ %li{ class: issue_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' %li - = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request' + = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do = icon('pencil-square-o') Edit diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml deleted file mode 100644 index e69de29bb2d..00000000000 --- a/app/views/projects/merge_requests/update.js.haml +++ /dev/null diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml index 62888e41935..ae13f8428f0 100644 --- a/app/views/projects/project_members/_shared_group_members.html.haml +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -8,7 +8,7 @@ group, members with %strong #{group_links.human_access} role (#{shared_group_users_count}) - - if current_user.can?(:admin_group, shared_group) + - if can?(current_user, :admin_group, shared_group) .panel-head-actions = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do %i.fa.fa-pencil-square-o diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index b38c5e18efb..9ce5562e667 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -2,4 +2,4 @@ %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line) + = markdown(label.description, pipeline: :single_line)
\ No newline at end of file diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml new file mode 100644 index 00000000000..dc89e36419c --- /dev/null +++ b/app/views/shared/_labels_row.html.haml @@ -0,0 +1,3 @@ +- labels.each do |label| + %span.label-row + = link_to_label(label, tooltip: false) diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index e3a6a5a68b6..d327bd0a96f 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -20,6 +20,11 @@ = sort_title_milestone_soon = link_to page_filter_path(sort: sort_value_milestone_later) do = sort_title_milestone_later + - if controller.controller_name == 'issues' || controller.action_name == 'issues' + = link_to page_filter_path(sort: sort_value_due_date_soon) do + = sort_title_due_date_soon + = link_to page_filter_path(sort: sort_value_due_date_later) do + = sort_title_due_date_later = link_to page_filter_path(sort: sort_value_upvotes) do = sort_title_upvotes = link_to page_filter_path(sort: sort_value_downvotes) do diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 921eaefd79a..fc1101646fb 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -23,6 +23,7 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" + .pull-right = render 'shared/sort_dropdown' @@ -46,9 +47,10 @@ .filter-item.inline = button_tag "Update issues", class: "btn update_selected_issues btn-save" -- if @label - .gray-content-block.second-block - = render "shared/label_row", label: @label + - if !@labels.nil? + .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } + - if @labels.any? + = render "shared/labels_row", labels: @labels :javascript new UsersSelect(); diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index f722e61eeac..61fd1e9c335 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,44 +1,14 @@ - if params[:label_name].present? - = hidden_field_tag(:label_name, params[:label_name]) + - if params[:label_name].respond_to?('any?') + - params[:label_name].each do |label| + = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %span.dropdown-toggle-text - = h(params[:label_name].presence || "Label") + = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - = dropdown_title("Filter by label") - = dropdown_filter("Search labels") - = dropdown_content - - if @project - = dropdown_footer do - %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project - %li - %a.dropdown-toggle-page{href: "#"} - Create new - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - - if can? current_user, :admin_label, @project - Manage labels - - else - View labels + = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } - if can? current_user, :admin_label, @project and @project - .dropdown-page-two.dropdown-new-label - = dropdown_title("Create new label", back: true) - = dropdown_content do - .dropdown-labels-error.js-label-error - %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} - .suggest-colors.suggest-colors-dropdown - - suggested_colors.each do |color| - = link_to '#', style: "background-color: #{color}", data: { color: color } do -   - .dropdown-label-color-input - .dropdown-label-color-preview.js-dropdown-label-color-preview - %input#new_label_color.dropdown-input-field{ type: "text" } - .clearfix - %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"} - Create - %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"} - Cancel + = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml new file mode 100644 index 00000000000..3bc57d3d2ac --- /dev/null +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -0,0 +1,17 @@ +.dropdown-page-two.dropdown-new-label + = dropdown_title("Create new label", back: true) + = dropdown_content do + .dropdown-labels-error.js-label-error + %input#new_label_name.default-dropdown-input{ type: "text", placeholder: "Name new label" } + .suggest-colors.suggest-colors-dropdown + - suggested_colors.each do |color| + = link_to '#', style: "background-color: #{color}", data: { color: color } do +   + .dropdown-label-color-input + .dropdown-label-color-preview.js-dropdown-label-color-preview + %input#new_label_color.default-dropdown-input{ type: "text" } + .clearfix + %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" } + Create + %button.btn.btn-default.pull-right.js-cancel-label-btn{ type: "button" } + Cancel diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml new file mode 100644 index 00000000000..7f4867417f7 --- /dev/null +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -0,0 +1,20 @@ +- title = local_assigns.fetch(:title, 'Assign labels') +- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') +.dropdown-page-one + = dropdown_title(title) + = dropdown_filter(filter_placeholder) + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels + = dropdown_loading
\ No newline at end of file diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index a6970b7eebb..1d9b09a5ef1 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ - else - page_context_word = 'issues' %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do #{state_filters_text_for(:opened, @project)} - if defined?(type) && type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do #{state_filters_text_for(:merged, @project)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do #{state_filters_text_for(:closed, @project)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do #{state_filters_text_for(:closed, @project)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do #{state_filters_text_for(:all, @project)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 03a615d191c..04b834ba5d1 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -10,17 +10,17 @@ = sidebar_gutter_toggle_icon .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - if prev_issuable = prev_issuable_for(issuable) - = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn' + = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Prev - if next_issuable = next_issuable_for(issuable) - = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn' + = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Next - = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)} - if issuable.assignee @@ -58,7 +58,7 @@ - if issuable.milestone = issuable.milestone.title - else - No + None .title.hide-collapsed Milestone = icon('spinner spin', class: 'block-loading') @@ -75,6 +75,34 @@ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) + - if issuable.has_attribute?(:due_date) + .block.due_date + .sidebar-collapsed-icon + = icon('calendar') + %span.js-due-date-sidebar-value + = issuable.due_date.try(:to_s, :medium) || 'None' + .title.hide-collapsed + Due date + = icon('spinner spin', class: 'block-loading') + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.bold.hide-collapsed + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + .light None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .selectbox.hide-collapsed + = f.hidden_field :due_date, value: issuable.due_date + .dropdown + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %span.dropdown-toggle-text Due date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title('Due date') + = dropdown_content do + .js-due-date-calendar + - if issuable.project.labels.any? .block.labels .sidebar-collapsed-icon @@ -101,24 +129,9 @@ Label = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - = dropdown_title("Assign labels") - = dropdown_filter("Search labels") - = dropdown_content - - if @project - = dropdown_footer do - %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project - %li - %a.dropdown-toggle-page{href: "#"} - Create new - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - - if can? current_user, :admin_label, @project - Manage labels - - else - View labels - = dropdown_loading + = render partial: "shared/issuable/label_page_default" + - if can? current_user, :admin_label, @project and @project + = render partial: "shared/issuable/label_page_create" = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -154,3 +167,4 @@ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') new Sidebar(); + new DueDateSelect(); diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index e7e04621ff4..1169bed0382 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,4 +1,5 @@ - @sort ||= sort_value_recently_updated +- personal = params[:personal] - archived = params[:archived] .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -10,7 +11,7 @@ Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do = title %li.divider @@ -20,3 +21,11 @@ %li = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects + - if current_user + %li.divider + %li + = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do + Owned by anyone + %li + = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do + Owned by me diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 53ff8959bc8..ab8b022411d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -6,9 +6,8 @@ - css_class = '' unless local_assigns[:css_class] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description -- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit - cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3'] -- cache_key.push(ci_commit.status) if ci_commit +- cache_key.push(project.commit.status) if project.commit.try(:status) %li.project-row{ class: css_class } = cache(cache_key) do @@ -16,9 +15,9 @@ - if project.main_language %span = project.main_language - - if ci_commit + - if project.commit.try(:status) %span - = render_ci_status(ci_commit) + = render_ci_status(project.commit) - if forks %span = icon('code-fork') diff --git a/bin/rails b/bin/rails index 5191e6927af..0138d79b751 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,9 @@ #!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' @@ -1,4 +1,9 @@ #!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/bin/rspec b/bin/rspec index 20060ebd79c..6e6709219af 100755 --- a/bin/rspec +++ b/bin/rspec @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end require 'bundler/setup' load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/spinach b/bin/spinach index a080e286cfe..474050e29d1 100755 --- a/bin/spinach +++ b/bin/spinach @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end require 'bundler/setup' load Gem.bin_path('spinach', 'spinach') diff --git a/bin/spring b/bin/spring index 7b45d374fcd..7fe232c3aae 100755 --- a/bin/spring +++ b/bin/spring @@ -4,12 +4,12 @@ # It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) - require "rubygems" - require "bundler" + require 'rubygems' + require 'bundler' - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) - Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } - gem "spring", match[1] - require "spring/binstub" + if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) + Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } + gem 'spring', match[1] + require 'spring/binstub' end end diff --git a/bin/teaspoon b/bin/teaspoon new file mode 100755 index 00000000000..7c3b8dfc4ed --- /dev/null +++ b/bin/teaspoon @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require 'bundler/setup' +load Gem.bin_path('teaspoon', 'teaspoon') diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 22fe51a4534..283936d0efc 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -107,6 +107,10 @@ if Gitlab::Metrics.enabled? config.instrument_methods(const) config.instrument_instance_methods(const) end + + # Instrument the classes used for checking if somebody has push access. + config.instrument_instance_methods(Gitlab::GitAccess) + config.instrument_instance_methods(Gitlab::GitAccessWiki) end GC::Profiler.enable diff --git a/config/routes.rb b/config/routes.rb index 79b62a0b1bb..2f820aafed1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -418,6 +418,7 @@ Rails.application.routes.draw do devise_scope :user do get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error + get '/users/almost_there' => 'confirmations#almost_there' end root to: "root#index" diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index e3ca2b4eea3..b99d24a03c9 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -19,7 +19,7 @@ class Gitlab::Seeder::Builds commits = @project.repository.commits('master', nil, 5) commits_sha = commits.map { |commit| commit.raw.id } commits_sha.map do |sha| - @project.ensure_ci_commit(sha) + @project.ensure_ci_commit(sha, 'master') end rescue [] diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb index 003169c13c6..d7b00e3d6ed 100644 --- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -4,6 +4,8 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? + create_trigrams_extension + unless trigrams_enabled? raise 'You must enable the pg_trgm extension. You can do so by running ' \ '"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \ @@ -37,6 +39,15 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration row && row['enabled'] == 't' ? true : false end + def create_trigrams_extension + # This may not work if the user doesn't have permission. We attempt in + # case we do have permission, particularly for test/dev environments. + begin + enable_extension 'pg_trgm' + rescue + end + end + def to_index { ci_runners: [:token, :description], diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb new file mode 100644 index 00000000000..ec08bd9fdfa --- /dev/null +++ b/db/migrate/20160310124959_add_due_date_to_issues.rb @@ -0,0 +1,6 @@ +class AddDueDateToIssues < ActiveRecord::Migration + def change + add_column :issues, :due_date, :date + add_index :issues, :due_date + end +end diff --git a/db/migrate/20160412173416_add_fields_to_ci_commit.rb b/db/migrate/20160412173416_add_fields_to_ci_commit.rb new file mode 100644 index 00000000000..125956a3ddd --- /dev/null +++ b/db/migrate/20160412173416_add_fields_to_ci_commit.rb @@ -0,0 +1,8 @@ +class AddFieldsToCiCommit < ActiveRecord::Migration + def change + add_column :ci_commits, :status, :string + add_column :ci_commits, :started_at, :timestamp + add_column :ci_commits, :finished_at, :timestamp + add_column :ci_commits, :duration, :integer + end +end diff --git a/db/migrate/20160412173417_update_ci_commit.rb b/db/migrate/20160412173417_update_ci_commit.rb new file mode 100644 index 00000000000..fd92444dbac --- /dev/null +++ b/db/migrate/20160412173417_update_ci_commit.rb @@ -0,0 +1,35 @@ +class UpdateCiCommit < ActiveRecord::Migration + # This migration can be run online, but needs to be executed for the second time after restarting Unicorn workers + # Otherwise Offline migration should be used. + def change + execute("UPDATE ci_commits SET status=#{status}, ref=#{ref}, tag=#{tag} WHERE status IS NULL") + end + + private + + def status + builds = '(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id)' + success = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='success')" + ignored = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND (status='failed' OR status='canceled') AND allow_failure)" + pending = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='pending')" + running = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='running')" + canceled = "(SELECT COUNT(*) FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id AND status='canceled')" + + "(CASE + WHEN #{builds}=0 THEN 'skipped' + WHEN #{builds}=#{success}+#{ignored} THEN 'success' + WHEN #{builds}=#{pending} THEN 'pending' + WHEN #{builds}=#{canceled} THEN 'canceled' + WHEN #{running}+#{pending}>0 THEN 'running' + ELSE 'failed' + END)" + end + + def ref + '(SELECT ref FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id ORDER BY id DESC LIMIT 1)' + end + + def tag + '(SELECT tag FROM ci_builds WHERE ci_builds.commit_id=ci_commits.id ORDER BY id DESC LIMIT 1)' + end +end diff --git a/db/migrate/20160412173418_add_ci_commit_indexes.rb b/db/migrate/20160412173418_add_ci_commit_indexes.rb new file mode 100644 index 00000000000..603d4a41610 --- /dev/null +++ b/db/migrate/20160412173418_add_ci_commit_indexes.rb @@ -0,0 +1,19 @@ +class AddCiCommitIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def change + add_index :ci_commits, [:gl_project_id, :sha], index_options + add_index :ci_commits, [:gl_project_id, :status], index_options + add_index :ci_commits, [:status], index_options + end + + private + + def index_options + if Gitlab::Database.postgresql? + { algorithm: :concurrently } + else + { } + end + end +end diff --git a/db/migrate/20160421130527_disable_repository_checks.rb b/db/migrate/20160421130527_disable_repository_checks.rb new file mode 100644 index 00000000000..808a4b93c7c --- /dev/null +++ b/db/migrate/20160421130527_disable_repository_checks.rb @@ -0,0 +1,11 @@ +class DisableRepositoryChecks < ActiveRecord::Migration + def up + change_column_default :application_settings, :repository_checks_enabled, false + execute 'UPDATE application_settings SET repository_checks_enabled = false' + end + + def down + change_column_default :application_settings, :repository_checks_enabled, true + execute 'UPDATE application_settings SET repository_checks_enabled = true' + end +end diff --git a/db/schema.rb b/db/schema.rb index d82c8c1e257..42457d92353 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160419120017) do +ActiveRecord::Schema.define(version: 20160421130527) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -77,7 +77,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" - t.boolean "repository_checks_enabled", default: true + t.boolean "repository_checks_enabled", default: false t.integer "metrics_packet_size", default: 1 t.text "shared_runners_text" end @@ -171,14 +171,21 @@ ActiveRecord::Schema.define(version: 20160419120017) do t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" + t.string "status" + t.datetime "started_at" + t.datetime "finished_at" + t.integer "duration" end + add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree + add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree + add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree create_table "ci_events", force: :cascade do |t| t.integer "project_id" @@ -422,6 +429,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" + t.date "due_date" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -431,6 +439,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree diff --git a/doc/README.md b/doc/README.md index e6ac4794827..e358da1c424 100644 --- a/doc/README.md +++ b/doc/README.md @@ -41,6 +41,8 @@ - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. - [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs +- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability ## Contributor documentation diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md new file mode 100644 index 00000000000..43d85ffb775 --- /dev/null +++ b/doc/administration/high_availability/README.md @@ -0,0 +1,35 @@ +# High Availability + +GitLab supports several different types of clustering and high-availability. +The solution you choose will be based on the level of scalability and +availability you require. The easiest solutions are scalable, but not necessarily +highly available. + +## Architecture + +### Active/Passive + +For pure high-availability/failover with no scaling you can use an +active/passive configuration. This utilizes DRBD (Distributed Replicated +Block Device) to keep all data in sync. DRBD requires a low latency link to +remain in sync. It is not advisable to attempt to run DRBD between data centers +or in different cloud availability zones. + +Components/Servers Required: + +- 2 servers/virtual machines (one active/one passive) + +### Active/Active + +This architecture scales easily because all application servers handle +user requests simultaneously. The database, Redis, and GitLab application are +all deployed on separate servers. The configuration is **only** highly-available +if the database, Redis and storage are also configured as such. + +**Steps to configure active/active:** + +1. [Configure the database](database.md) +1. [Configure Redis](redis.md) +1. [Configure NFS](nfs.md) +1. [Configure the GitLab application servers](gitlab.md) +1. [Configure the load balancers](load_balancer.md) diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md new file mode 100644 index 00000000000..538dada1bae --- /dev/null +++ b/doc/administration/high_availability/database.md @@ -0,0 +1,116 @@ +# Configuring a Database for GitLab HA + +You can choose to install and manage a database server (PostgreSQL/MySQL) +yourself, or you can use GitLab Omnibus packages to help. GitLab recommends +PostgreSQL. This is the database that will be installed if you use the +Omnibus package to manage your database. + +## Configure your own database server + +If you're hosting GitLab on a cloud provider, you can optionally use a +managed service for PostgreSQL. For example, AWS offers a managed Relational +Database Service (RDS) that runs PostgreSQL. + +If you use a cloud-managed service, or provide your own PostgreSQL: + +1. Set up a `gitlab` username with a password of your choice. The `gitlab` user + needs privileges to create the `gitlabhq_production` database. +1. Configure the GitLab application servers with the appropriate details. + This step is covered in [Configuring GitLab for HA](gitlab.md) + +## Configure using Omnibus + +1. Download/install GitLab Omnibus using **steps 1 and 2** from + [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other + steps on the download page. +1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration. + Be sure to change the `external_url` to match your eventual GitLab front-end + URL. + + ```ruby + external_url 'https://gitlab.example.com' + + # Disable all components except PostgreSQL + postgresql['enable'] = true + bootstrap['enable'] = false + nginx['enable'] = false + unicorn['enable'] = false + sidekiq['enable'] = false + redis['enable'] = false + gitlab_workhorse['enable'] = false + mailroom['enable'] = false + + # PostgreSQL configuration + postgresql['sql_password'] = 'DB password' + postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0'] + postgresql['listen_address'] = '0.0.0.0' + ``` + +1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. + + > **Note**: This `reconfigure` step will result in some errors. + That's OK - don't be alarmed. + +1. Open a database prompt: + + ``` + su - gitlab-psql + /bin/bash + psql -h /var/opt/gitlab/postgresql -d template1 + + # Output: + + psql (9.2.15) + Type "help" for help. + + template1=# + ``` + +1. Run the following command at the database prompt and you will be asked to + enter the new password for the PostgreSQL superuser. + + ``` + \password + + # Output: + + Enter new password: + Enter it again: + ``` + +1. Similarly, set the password for the `gitlab` database user. Use the same + password that you specified in the `/etc/gitlab/gitlab.rb` file for + `postgresql['sql_password']`. + + ``` + \password gitlab + + # Output: + + Enter new password: + Enter it again: + ``` + +1. Enable the `pg_trgm` extension: + ``` + CREATE EXTENSION pg_trgm; + + # Output: + + CREATE EXTENSION + ``` +1. Exit the database prompt by typing `\q` and Enter. +1. Exit the `gitlab-psql` user by running `exit` twice. +1. Run `sudo gitlab-ctl reconfigure` a final time. +1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations + from running on upgrade. Only the primary GitLab application server should + handle migrations. + +--- + +Read more on high-availability configuration: + +1. [Configure Redis](redis.md) +1. [Configure NFS](nfs.md) +1. [Configure the GitLab application servers](gitlab.md) +1. [Configure the load balancers](load_balancer.md) diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md new file mode 100644 index 00000000000..8a881ce8863 --- /dev/null +++ b/doc/administration/high_availability/gitlab.md @@ -0,0 +1,131 @@ +# Configuring GitLab for HA + +Assuming you have already configured a database, Redis, and NFS, you can +configure the GitLab application server(s) now. Complete the steps below +for each GitLab application server in your environment. + +> **Note:** There is some additional configuration near the bottom for + secondary GitLab application servers. It's important to read and understand + these additional steps before proceeding with GitLab installation. + +1. If necessary, install the NFS client utility packages using the following + commands: + + ``` + # Ubuntu/Debian + apt-get install nfs-common + + # CentOS/Red Hat + yum install nfs-utils nfs-utils-lib + ``` + +1. Specify the necessary NFS shares. Mounts are specified in + `/etc/fstab`. The exact contents of `/etc/fstab` will depend on how you chose + to configure your NFS server. See [NFS documentation](nfs.md) for the various + options. Here is an example snippet to add to `/etc/fstab`: + + ``` + 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 + 10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 + ``` + +1. Create the shared directories. These may be different depending on your NFS + mount locations. + + ``` + mkdir -p /var/opt/gitlab/.ssh /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/git-data + ``` + +1. Download/install GitLab Omnibus using **steps 1 and 2** from + [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other + steps on the download page. +1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration. + Be sure to change the `external_url` to match your eventual GitLab front-end + URL. Depending your the NFS configuration, you may need to change some GitLab + data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb` + configuration values for various scenarios. The example below assumes you've + added NFS mounts in the default data locations. + + ```ruby + external_url 'https://gitlab.example.com' + + # Prevent GitLab from starting if NFS data mounts are not available + high_availability['mountpoint'] = '/var/opt/gitlab/git-data' + + # Disable components that will not be on the GitLab application server + postgresql['enable'] = false + redis['enable'] = false + + # PostgreSQL connection details + gitlab_rails['db_adapter'] = 'postgresql' + gitlab_rails['db_encoding'] = 'unicode' + gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server + gitlab_rails['db_password'] = 'DB password' + + # Redis connection details + gitlab_rails['redis_port'] = '6379' + gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server + gitlab_rails['redis_password'] = 'Redis Password' + ``` + +1. Run `sudo gitlab-ctl reconfigure` to compile the configuration. + +## Primary GitLab application server + +As a final step, run the setup rake task on the first GitLab application server. +It is not necessary to run this on additional application servers. + +1. Initialize the database by running `sudo gitlab-rake gitlab:setup`. + +> **WARNING:** Only run this setup task on **NEW** GitLab instances because it + will wipe any existing data. + +> **Note:** When you specify `https` in the `external_url`, as in the example + above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If + certificates are not present, Nginx will fail to start. See + [Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) + for more information. + +## Additional configuration for secondary GitLab application servers + +Secondary GitLab servers (servers configured **after** the first GitLab server) +need some additional configuration. + +1. Configure shared secrets. These values can be obtained from the primary + GitLab server in `/etc/gitlab/gitlab-secrets.json`. Add these to + `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure` in + the steps above. + + ```ruby + gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860' + gitlab_rails['secret_token'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa' + gitlab_ci['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d' + gitlab_ci['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964' + ``` + +1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations + from running on upgrade. Only the primary GitLab application server should + handle migrations. + +## Troubleshooting + +- `mount: wrong fs type, bad option, bad superblock on` + +You have not installed the necessary NFS client utilities. See step 1 above. + +- `mount: mount point /var/opt/gitlab/... does not exist` + +This particular directory does not exist on the NFS server. Ensure +the share is exported and exists on the NFS server and try to remount. + +--- + +Read more on high-availability configuration: + +1. [Configure the database](database.md) +1. [Configure Redis](redis.md) +1. [Configure NFS](nfs.md) +1. [Configure the load balancers](load_balancer.md) diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md new file mode 100644 index 00000000000..b1fe34ed9a1 --- /dev/null +++ b/doc/administration/high_availability/load_balancer.md @@ -0,0 +1,63 @@ +# Load Balancer for GitLab HA + +In an active/active GitLab configuration, you will need a load balancer to route +traffic to the application servers. The specifics on which load balancer to use +or the exact configuration is beyond the scope of GitLab documentation. We hope +that if you're managing HA systems like GitLab you have a load balancer of +choice already. Some examples including HAProxy (open-source), F5 Big-IP LTM, +and Citrix Net Scaler. This documentation will outline what ports and protocols +you need to use with GitLab. + +## Basic ports + +| LB Port | Backend Port | Protocol | +| ------- | ------------ | -------- | +| 80 | 80 | HTTP | +| 443 | 443 | HTTPS [^1] | +| 22 | 22 | TCP | + +## GitLab Pages Ports + +If you're using GitLab Pages you will need some additional port configurations. +GitLab Pages requires a separate VIP. Configure DNS to point the +`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new VIP. See the +[GitLab Pages documentation][gitlab-pages] for more information. + +| LB Port | Backend Port | Protocol | +| ------- | ------------ | -------- | +| 80 | Varies [^2] | HTTP | +| 443 | Varies [^2] | TCP [^3] | + +## Alternate SSH Port + +Some organizations have policies against opening SSH port 22. In this case, +it may be helpful to configure an alternate SSH hostname that allows users +to use SSH on port 443. An alternate SSH hostname will require a new VIP +compared to the other GitLab HTTP configuration above. + +Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com. + +| LB Port | Backend Port | Protocol | +| ------- | ------------ | -------- | +| 443 | 22 | TCP | + +--- + +Read more on high-availability configuration: + +1. [Configure the database](database.md) +1. [Configure Redis](redis.md) +1. [Configure NFS](nfs.md) +1. [Configure the GitLab application servers](gitlab.md) + +[^1]: When using HTTPS protocol for port 443, you will need to add an SSL + certificate to the load balancers. If you wish to terminate SSL at the + GitLab application server instead, use TCP protocol. +[^2]: The backend port for GitLab Pages depends on the + `gitlab_pages['external_http']` and `gitlab_pages['external_https']` + setting. See [GitLab Pages documentation][gitlab-pages] for more details. +[^3]: Port 443 for GitLab Pages should always use the TCP protocol. Users can + configure custom domains with custom SSL, which would not be possible + if SSL was terminated at the load balancer. + +[gitlab-pages]: http://doc.gitlab.com/ee/pages/administration.html diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md new file mode 100644 index 00000000000..e4e124e200a --- /dev/null +++ b/doc/administration/high_availability/nfs.md @@ -0,0 +1,116 @@ +# NFS + +## Required NFS Server features + +**File locking**: GitLab **requires** file locking which is only supported +natively in NFS version 4. NFSv3 also supports locking as long as +Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not +specifically test NFSv3. + +**no_root_squash**: NFS normally changes the `root` user to `nobody`. This is +a good security measure when NFS shares will be accessed by many different +users. However, in this case only GitLab will use the NFS share so it +is safe. GitLab requires the `no_root_squash` setting because we need to +manage file permissions automatically. Without the setting you will receive +errors when the Omnibus package tries to alter permissions. Note that GitLab +and other bundled components do **not** run as `root` but as non-privileged +users. The requirement for `no_root_squash` is to allow the Omnibus package to +set ownership and permissions on files, as needed. + +### Recommended options + +When you define your NFS exports, we recommend you also add the following +options: + +- `sync` - Force synchronous behavior. Default is asynchronous and under certain + circumstances it could lead to data loss if a failure occurs before data has + synced. + +## Client mount options + +Below is an example of an NFS mount point we use on GitLab.com: + +``` +10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 +``` + +Notice several options that you should consider using: + +| Setting | Description | +| ------- | ----------- | +| `nobootwait` | Don't halt boot process waiting for this mount to become available +| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously. + +## Mount locations + +When using default Omnibus configuration you will need to share 5 data locations +between all GitLab cluster nodes. No other locations should be shared. The +following are the 5 locations you need to mount: + +| Location | Description | +| -------- | ----------- | +| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data +| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services +| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments +| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data +| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces + +Other GitLab directories should not be shared between nodes. They contain +node-specific files and GitLab code that does not need to be shared. To ship +logs to a central location consider using remote syslog. GitLab Omnibus packages +provide configuration for [UDP log shipping][udp-log-shipping]. + +### Consolidating mount points + +If you don't want to configure 5-6 different NFS mount points, you have a few +alternative options. + +#### Change default file locations + +Omnibus allows you to configure the file locations. With custom configuration +you can specify just one main mountpoint and have all of these locations +as subdirectories. Mount `/gitlab-data` then use the following Omnibus +configuration to move each data location to a subdirectory: + +```ruby +user['home'] = '/gitlab-data/home' +git_data_dir '/gitlab-data/git-data' +gitlab_rails['shared_path'] = '/gitlab-data/shared' +gitlab_rails['uploads_directory'] = "/gitlab-data/uploads" +gitlab_ci['builds_directory'] = '/gitlab-data/builds' +``` + +To move the `git` home directory, all GitLab services must be stopped. Run +`gitlab-ctl stop && initctl stop gitlab-runsvdir`. Then continue with the +reconfigure. + +Run `sudo gitlab-ctl reconfigure` to start using the central location. Please +be aware that if you had existing data you will need to manually copy/rsync it +to these new locations and then restart GitLab. + +#### Bind mounts + +Bind mounts provide a way to specify just one NFS mount and then +bind the default GitLab data locations to the NFS mount. Start by defining your +single NFS mount point as you normally would in `/etc/fstab`. Let's assume your +NFS mount point is `/gitlab-data`. Then, add the following bind mounts in +`/etc/fstab`: + +```bash +/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0 +/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0 +/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0 +/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0 +/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0 +``` + +--- + +Read more on high-availability configuration: + +1. [Configure the database](database.md) +1. [Configure Redis](redis.md) +1. [Configure the GitLab application servers](gitlab.md) +1. [Configure the load balancers](load_balancer.md) + +[udp-log-shipping]: http://doc.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md new file mode 100644 index 00000000000..d89a1e582ca --- /dev/null +++ b/doc/administration/high_availability/redis.md @@ -0,0 +1,62 @@ +# Configuring Redis for GitLab HA + +You can choose to install and manage Redis yourself, or you can use GitLab +Omnibus packages to help. + +## Configure your own Redis server + +If you're hosting GitLab on a cloud provider, you can optionally use a +managed service for Redis. For example, AWS offers a managed ElastiCache service +that runs Redis. + +> **Note:** Redis does not require authentication by default. See + [Redis Security](http://redis.io/topics/security) documentation for more + information. We recommend using a combination of a Redis password and tight + firewall rules to secure your Redis service. + +## Configure using Omnibus + +1. Download/install GitLab Omnibus using **steps 1 and 2** from + [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other + steps on the download page. +1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration. + Be sure to change the `external_url` to match your eventual GitLab front-end + URL. + + ```ruby + external_url 'https://gitlab.example.com' + + # Disable all components except PostgreSQL + redis['enable'] = true + bootstrap['enable'] = false + nginx['enable'] = false + unicorn['enable'] = false + sidekiq['enable'] = false + postgresql['enable'] = false + gitlab_workhorse['enable'] = false + mailroom['enable'] = false + + # Redis configuration + redis['port'] = 6379 + redis['bind'] = '0.0.0.0' + + # If you wish to use Redis authentication (recommended) + redis['password'] = 'Redis Password' + ``` + +1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. + + > **Note**: This `reconfigure` step will result in some errors. + That's OK - don't be alarmed. +1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations + from running on upgrade. Only the primary GitLab application server should + handle migrations. + +--- + +Read more on high-availability configuration: + +1. [Configure the database](database.md) +1. [Configure NFS](nfs.md) +1. [Configure the GitLab application servers](gitlab.md) +1. [Configure the load balancers](load_balancer.md) diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 61bf8ce6161..3411e4af6a7 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -1,7 +1,8 @@ # Repository checks >**Note:** -This feature was [introduced][ce-3232] in GitLab 8.7. +This feature was [introduced][ce-3232] in GitLab 8.7. It is OFF by +default because it still causes too many false alarms. Git has a built-in mechanism, [git fsck][git-fsck], to verify the integrity of all data commited to a repository. GitLab administrators diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md new file mode 100644 index 00000000000..134a7583762 --- /dev/null +++ b/doc/administration/troubleshooting/sidekiq.md @@ -0,0 +1,162 @@ +# Troubleshooting Sidekiq + +Sidekiq is the background job processor GitLab uses to asynchronously run +tasks. When things go wrong it can be difficult to troubleshoot. These +situations also tend to be high-pressure because a production system job queue +may be filling up. Users will notice when this happens because new branches +may not show up and merge requests may not be updated. The following are some +troubleshooting steps that will help you diagnose the bottleneck. + +> **Note:** GitLab administrators/users should consider working through these +debug steps with GitLab Support so the backtraces can be analyzed by our team. +It may reveal a bug or necessary improvement in GitLab. + +> **Note:** In any of the backtraces, be weary of suspecting cases where every + thread appears to be waiting in the database, Redis, or waiting to acquire + a mutex. This **may** mean there's contention in the database, for example, + but look for one thread that is different than the rest. This other thread + may be using all available CPU, or have a Ruby Global Interpreter Lock, + preventing other threads from continuing. + +## Thread dump + +Send the Sidekiq process ID the `TTIN` signal and it will output thread +backtraces in the log file. + +``` +kill -TTIN <sidekiq_pid> +``` + +Check in `/var/log/gitlab/sidekiq/current` or `$GITLAB_HOME/log/sidekiq.log` for +the backtrace output. The backtraces will be lengthy and generally start with +several `WARN` level messages. Here's an example of a single thread's backtrace: + +``` +2016-04-13T06:21:20.022Z 31517 TID-orn4urby0 WARN: ActiveRecord::RecordNotFound: Couldn't find Note with 'id'=3375386 +2016-04-13T06:21:20.022Z 31517 TID-orn4urby0 WARN: /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/activerecord-4.2.5.2/lib/active_record/core.rb:155:in `find' +/opt/gitlab/embedded/service/gitlab-rails/app/workers/new_note_worker.rb:7:in `perform' +/opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/sidekiq-4.0.1/lib/sidekiq/processor.rb:150:in `execute_job' +/opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/sidekiq-4.0.1/lib/sidekiq/processor.rb:132:in `block (2 levels) in process' +/opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/sidekiq-4.0.1/lib/sidekiq/middleware/chain.rb:127:in `block in invoke' +/opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/sidekiq_middleware/memory_killer.rb:17:in `call' +/opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/sidekiq-4.0.1/lib/sidekiq/middleware/chain.rb:129:in `block in invoke' +/opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/sidekiq_middleware/arguments_logger.rb:6:in `call' +... +``` + +In some cases Sidekiq may be hung and unable to respond to the `TTIN` signal. +Move on to other troubleshooting methods if this happens. + +## Process profiling with `perf` + +Linux has a process profiling tool called `perf` that is helpful when a certain +process is eating up a lot of CPU. If you see high CPU usage and Sidekiq won't +respond to the `TTIN` signal, this is a good next step. + +If `perf` is not installed on your system, install it with `apt-get` or `yum`: + +``` +# Debian +sudo apt-get install linux-tools + +# Ubuntu (may require these additional Kernel packages) +sudo apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r` + +# Red Hat/CentOS +sudo yum install perf +``` + +Run perf against the Sidekiq PID: + +``` +sudo perf record -p <sidekiq_pid> +``` + +Let this run for 30-60 seconds and then press Ctrl-C. Then view the perf report: + +``` +sudo perf report + +# Sample output +Samples: 348K of event 'cycles', Event count (approx.): 280908431073 + 97.69% ruby nokogiri.so [.] xmlXPathNodeSetMergeAndClear + 0.18% ruby libruby.so.2.1.0 [.] objspace_malloc_increase + 0.12% ruby libc-2.12.so [.] _int_malloc + 0.10% ruby libc-2.12.so [.] _int_free +``` + +Above you see sample output from a perf report. It shows that 97% of the CPU is +being spent inside Nokogiri and `xmlXPathNodeSetMergeAndClear`. For something +this obvious you should then go investigate what job in GitLab would use +Nokogiri and XPath. Combine with `TTIN` or `gdb` output to show the +corresponding Ruby code where this is happening. + +## The GNU Project Debugger (gdb) + +`gdb` can be another effective tool for debugging Sidekiq. It gives you a little +more interactive way to look at each thread and see what's causing problems. + +> **Note:** Attaching to a process with `gdb` will suspends the normal operation + of the process (Sidekiq will not process jobs while `gdb` is attached). + +Start by attaching to the Sidekiq PID: + +``` +gdb -p <sidekiq_pid> +``` + +Then gather information on all the threads: + +``` +info threads + +# Example output +30 Thread 0x7fe5fbd63700 (LWP 26060) 0x0000003f7cadf113 in poll () from /lib64/libc.so.6 +29 Thread 0x7fe5f2b3b700 (LWP 26533) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +28 Thread 0x7fe5f2a3a700 (LWP 26534) 0x0000003f7ce0ba5e in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +27 Thread 0x7fe5f2939700 (LWP 26535) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +26 Thread 0x7fe5f2838700 (LWP 26537) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +25 Thread 0x7fe5f2737700 (LWP 26538) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +24 Thread 0x7fe5f2535700 (LWP 26540) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +23 Thread 0x7fe5f2434700 (LWP 26541) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +22 Thread 0x7fe5f2232700 (LWP 26543) 0x0000003f7ce0b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +21 Thread 0x7fe5f2131700 (LWP 26544) 0x00007fe5f7b570f0 in xmlXPathNodeSetMergeAndClear () +from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +... +``` + +If you see a suspicious thread, like the Nokogiri one above, you may want +to get more information: + +``` +thread 21 +bt + +# Example output +#0 0x00007ff0d6afe111 in xmlXPathNodeSetMergeAndClear () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#1 0x00007ff0d6b0b836 in xmlXPathNodeCollectAndTest () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#2 0x00007ff0d6b09037 in xmlXPathCompOpEval () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#3 0x00007ff0d6b09017 in xmlXPathCompOpEval () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#4 0x00007ff0d6b092e0 in xmlXPathCompOpEval () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#5 0x00007ff0d6b0bc37 in xmlXPathRunEval () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#6 0x00007ff0d6b0be5f in xmlXPathEvalExpression () from /opt/gitlab/embedded/service/gem/ruby/2.1.0/gems/nokogiri-1.6.7.2/lib/nokogiri/nokogiri.so +#7 0x00007ff0d6a97dc3 in evaluate (argc=2, argv=0x1022d058, self=<value optimized out>) at xml_xpath_context.c:221 +#8 0x00007ff0daeab0ea in vm_call_cfunc_with_frame (th=0x1022a4f0, reg_cfp=0x1032b810, ci=<value optimized out>) at vm_insnhelper.c:1510 +``` + +To output a backtrace from all threads at once: + +``` +apply all thread bt +``` + +## Check for blocking queries + +Sometimes the speed at which Sidekiq processes jobs can be so fast that it can +cause database contention. Check for blocking queries when backtraces above +show that many threads are stuck in the database adapter. + +The PostgreSQL wiki has details on the query you can run to see blocking +queries. The query is different based on PostgreSQL version. See +[Lock Monitoring](https://wiki.postgresql.org/wiki/Lock_Monitoring) for +the query details. diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md new file mode 100644 index 00000000000..3625c4191b8 --- /dev/null +++ b/doc/downgrade_ee_to_ce/README.md @@ -0,0 +1,82 @@ +# Downgrading from EE to CE + +If you ever decide to downgrade your Enterprise Edition back to the Community +Edition, there are a few steps you need take before installing the CE package +on top of the current EE package, or, if you are in an installation from source, +before you change remotes and fetch the latest CE code. + +## Disable Enterprise-only features + +First thing to do is to disable the following features. + +### Authentication mechanisms + +Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so +you should disable these mechanisms before downgrading and you should provide +alternative authentication methods to your users. + +### Git Annex + +Git Annex is also only available on the Enterprise Edition. This means that if +you have repositories that use Git Annex to store large files, these files will +no longer be easily available via Git. You should consider migrating these +repositories to use Git LFS before downgrading to the Community Edition. + +### Remove Jenkins CI Service entries from the database + +The `JenkinsService` class is only available on the Enterprise Edition codebase, +so if you downgrade to the Community Edition, you'll come across the following +error: + +``` +Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms) + +ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'JenkinsService'. This +error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this +column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to +use another column for that information.) +``` + +All services are created automatically for every project you have, so in order +to avoid getting this error, you need to remove all instances of the +`JenkinsService` from your database: + +**Omnibus Installation** + +``` +$ sudo gitlab-rails runner "Service.where(type: 'JenkinsService').delete_all" +``` + +**Source Installation** + +``` +$ bundle exec rails runner "Service.where(type: 'JenkinsService').delete_all" production +``` + +## Downgrade to CE + +After performing the above mentioned steps, you are now ready to downgrade your +GitLab installation to the Community Edition. + +**Omnibus Installation** + +To downgrade an Omnibus installation, it is sufficient to install the Community +Edition package on top of the currently installed one. You can do this manually, +by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce) +you need, or by adding our CE package repository and following the +[CE installation instructions](https://about.gitlab.com/downloads/). + +**Source Installation** + +To downgrade a source installation, you need to replace the current remote of +your GitLab installation with the Community Edition's remote, fetch the latest +changes, and checkout the latest stable branch: + +``` +$ git remote set-url origin git@gitlab.com:gitlab-org/gitlab-ce.git +$ git fetch --all +$ git checkout 8-x-stable +``` + +Remember to follow the correct [update guides](../update/README.md) to make +sure all dependencies are up to date. diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md index 8599133a726..4a2c6ea91d2 100644 --- a/doc/update/8.6-to-8.7.md +++ b/doc/update/8.6-to-8.7.md @@ -45,8 +45,8 @@ sudo -u git -H git checkout 8-7-stable-ee ```bash cd /home/git/gitlab-shell -sudo -u git -H git fetch --all -sudo -u git -H git checkout v2.7.0 +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v2.7.2 ``` ### 5. Update gitlab-workhorse diff --git a/doc/update/README.md b/doc/update/README.md index 0241f036830..a770633c9b8 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -1,18 +1,95 @@ -Depending on the installation method and your GitLab version, there are multiple update guides. Choose one that fits your needs. +# Updating GitLab + +Depending on the installation method and your GitLab version, there are multiple +update guides. + +There are currently 3 official ways to install GitLab: + +- Omnibus packages +- Source installation +- Docker installation + +Based on your installation, choose a section below that fits your needs. + +--- + +<!-- START doctoc generated TOC please keep comment here to allow auto update --> +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Omnibus Packages](#omnibus-packages) +- [Installation from source](#installation-from-source) +- [Installation using Docker](#installation-using-docker) +- [Upgrading between editions](#upgrading-between-editions) + - [Community to Enterprise Edition](#community-to-enterprise-edition) + - [Enterprise to Community Edition](#enterprise-to-community-edition) +- [Miscellaneous](#miscellaneous) + +<!-- END doctoc generated TOC please keep comment here to allow auto update --> ## Omnibus Packages -- [Omnibus update guide](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md) contains the steps needed to update a GitLab [package](https://about.gitlab.com/downloads/). +- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html) + contains the steps needed to update an Omnibus GitLab package. ## Installation from source -- [The individual upgrade guides](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update) are for those who have installed GitLab from source. -- [The CE to EE update guides](https://gitlab.com/subscribers/gitlab-ee/tree/master/doc/update) are for subscribers of the Enterprise Edition only. The steps are very similar to a version upgrade: stop the server, get the code, update config files for the new functionality, install libs and do migrations, update the init script, start the application and check the application status. -- [Upgrader](upgrader.md) is an automatic ruby script that performs the update for installations from source. -- [Patch versions](patch_versions.md) guide includes the steps needed for a patch version, eg. 6.2.0 to 6.2.1. +- [Upgrading Community Edition from source][source-ce] - The individual + upgrade guides are for those who have installed GitLab CE from source. +- [Upgrading Enterprise Edition from source][source-ee] - The individual + upgrade guides are for those who have installed GitLab EE from source. +- [Patch versions](patch_versions.md) guide includes the steps needed for a + patch version, eg. 6.2.0 to 6.2.1, and apply to both Community and Enterprise + Editions. + +## Installation using Docker + +GitLab provides official Docker images for both Community and Enterprise +editions. They are based on the Omnibus package and instructions on how to +update them are in [a separate document][omnidocker]. + +## Upgrading between editions + +GitLab comes in two flavors: [Community Edition][ce] which is MIT licensed, +and [Enterprise Edition][ee] which builds on top of the Community Edition and +includes extra features mainly aimed at organizations with more than 100 users. + +Below you can find some guides to help you change editions easily. + +### Community to Enterprise Edition + +>**Note:** +The following guides are for subscribers of the Enterprise Edition only. + +If you wish to upgrade your GitLab installation from Community to Enterprise +Edition, follow the guides below based on the installation method: + +- [Source CE to EE update guides][source-ee] - Find your version, and follow the + `-ce-to-ee.md` guide. The steps are very similar to a version upgrade: stop + the server, get the code, update config files for the new functionality, + install libraries and do migrations, update the init script, start the + application and check its status. +- [Omnibus CE to EE][omni-ce-ee] - Follow this guide to update your Omnibus + GitLab Community Edition to the Enterprise Edition. + +### Enterprise to Community Edition + +If you need to downgrade your Enterprise Edition installation back to Community +Edition, you can follow [this guide][ee-ce] to make the process as smooth as +possible. ## Miscellaneous -- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostgreSQL. -- [MySQL installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database. +- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating + your database from MySQL to PostgreSQL. +- [MySQL installation guide](../install/database_mysql.md) contains additional + information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) + +[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html +[source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update +[source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update +[ee-ce]: ../downgrade_ee_to_ce/README.md +[ce]: https://about.gitlab.com/features/#community +[ee]: https://about.gitlab.com/features/#enterprise +[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md index b0ca0879643..4a499009842 100644 --- a/doc/workflow/cherry_pick_changes.md +++ b/doc/workflow/cherry_pick_changes.md @@ -1,6 +1,7 @@ # Cherry-pick changes -_**Note:** This feature was [introduced][ce-3514] in GitLab 8.7._ +>**Note:** +This feature was [introduced][ce-3514] in GitLab 8.7. --- diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index f693f430a42..e670e415c71 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -1,7 +1,8 @@ # Import your project from GitHub to GitLab
-_**Note:** In order to enable the GitHub import setting, you should first
-enable the [GitHub integration][gh-import] in your GitLab instance._
+>**Note:**
+In order to enable the GitHub import setting, you should first
+enable the [GitHub integration][gh-import] in your GitLab instance.
At its current state, GitHub importer can import:
@@ -10,10 +11,13 @@ At its current state, GitHub importer can import: - the issues (introduced in GitLab 7.7)
- the pull requests (introduced in GitLab 8.4)
- the wiki pages (introduced in GitLab 8.4)
+- the milestones (introduced in GitLab 8.7)
+- the labels (introduced in GitLab 8.7)
-It is not yet possible to import your labels, milestones and cross-repository
-pull requests (those from forks). We are working on improving this in the near
-future.
+With GitLab 8.7+, references to pull requests and issues are preserved.
+
+It is not yet possible to import your cross-repository pull requests (those from
+forks). We are working on improving this in the near future.
The importer page is visible when you [create a new project][new-project].
Click on the **GitHub** link and you will be redirected to GitHub for
diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature index e07f8053fb7..49d7a3b9af2 100644 --- a/features/project/issues/filter_labels.feature +++ b/features/project/issues/filter_labels.feature @@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels @javascript Scenario: I filter by one label Given I click link "bug" + And I click "dropdown close button" Then I should see "Bugfix1" in issues list And I should see "Bugfix2" in issues list And I should not see "Feature1" in issues list diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 4724a326277..3b59089a093 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -22,4 +22,8 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps step 'the active main tab should be Audit Log' do ensure_active_main_tab('Audit Log') end + + def ensure_active_main_tab(content) + expect(find('.layout-nav li.active')).to have_content(content) + end end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index 6d50501a722..d82c6856918 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps page.find('.js-label-select').click sleep 0.5 execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + end + + step 'I click "dropdown close button"' do + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click sleep 2 end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 4f883fe7c27..3b1a00f628a 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -519,7 +519,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id + ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch create :ci_build, commit: ci_commit end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index c4c7672a432..cf30e23b6bd 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -10,16 +10,16 @@ module SharedBuilds end step 'project has a recent build' do - @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha) + @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master') @build = create(:ci_build_with_coverage, commit: @ci_commit) end step 'recent build is successful' do - @build.update_column(:status, 'success') + @build.update(status: 'success') end step 'recent build failed' do - @build.update_column(:status, 'failed') + @build.update(status: 'failed') end step 'project has another build that is running' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b13e82f276b..ea5f9580308 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -230,7 +230,7 @@ module SharedProject step 'project "Shop" has CI build' do project = Project.find_by(name: "Shop") - create :ci_commit, project: project, sha: project.commit.sha + create :ci_commit, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' end step 'I should see last commit with CI status' do diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 8e74e177ea0..7388ed2f4ea 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -21,10 +21,9 @@ module API authorize!(:read_commit_status, user_project) not_found!('Commit') unless user_project.commit(params[:sha]) - ci_commit = user_project.ci_commit(params[:sha]) - return [] unless ci_commit - statuses = ci_commit.statuses + ci_commits = user_project.ci_commits.where(sha: params[:sha]) + statuses = ::CommitStatus.where(commit: ci_commits) statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? statuses = statuses.where(stage: params[:stage]) if params[:stage].present? @@ -51,7 +50,21 @@ module API commit = @project.commit(params[:sha]) not_found! 'Commit' unless commit - ci_commit = @project.ensure_ci_commit(commit.sha) + # Since the CommitStatus is attached to Ci::Commit (in the future Pipeline) + # We need to always have the pipeline object + # To have a valid pipeline object that can be attached to specific MR + # Other CI service needs to send `ref` + # If we don't receive it, we will attach the CommitStatus to + # the first found branch on that commit + + ref = params[:ref] + unless ref + branches = @project.repository.branch_names_contains(commit.sha) + not_found! 'References for commit' if branches.none? + ref = branches.first + end + + ci_commit = @project.ensure_ci_commit(commit.sha, ref) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 8cfa1f1556b..3ac7b50c4ce 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -23,7 +23,7 @@ module API end post "/allowed" do - Gitlab::Metrics.tag_transaction('action', 'Grape#/internal/allowed') + Gitlab::Metrics.action = 'Grape#/internal/allowed' status 200 diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index cf9938d25a7..ccca65cbe1c 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -103,10 +103,10 @@ module API required_attributes! [:hook_id] begin - @hook = ProjectHook.find(params[:hook_id]) - @hook.destroy + @hook = user_project.hooks.destroy(params[:hook_id]) rescue # ProjectHook can raise Error if hook_id not found + not_found!("Error deleting hook #{params[:hook_id]}") end end end diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index d179bea181e..38c4219518e 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -1,7 +1,6 @@ module Banzai module Filter - # HTML Filter to add a `rel="nofollow"` attribute to external links - # + # HTML Filter to modify the attributes of external links class ExternalLinkFilter < HTML::Pipeline::Filter def call doc.search('a').each do |node| @@ -15,7 +14,7 @@ module Banzai # Skip internal links next if link.start_with?(internal_url) - node.set_attribute('rel', 'nofollow') + node.set_attribute('rel', 'nofollow noreferrer') end doc diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index a2987850d03..8488a493b55 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -18,9 +18,7 @@ module Banzai def references_in(text, pattern = Label.reference_pattern) text.gsub(pattern) do |match| - project = project_from_ref($~[:project]) - params = label_params($~[:label_id].to_i, $~[:label_name]) - label = project.labels.find_by(params) + label = find_label($~[:project], $~[:label_id], $~[:label_name]) if label yield match, label.id, $~[:project], $~ @@ -30,18 +28,12 @@ module Banzai end end - def url_for_object(label, project) - h = Gitlab::Routing.url_helpers - h.namespace_project_issues_url(project.namespace, project, label_name: label.name, - only_path: context[:only_path]) - end + def find_label(project_ref, label_id, label_name) + project = project_from_ref(project_ref) + return unless project - def object_link_text(object, matches) - if context[:project] == object.project - LabelsHelper.render_colored_label(object) - else - LabelsHelper.render_colored_cross_project_label(object) - end + label_params = label_params(label_id, label_name) + project.labels.find_by(label_params) end # Parameters to pass to `Label.find_by` based on the given arguments @@ -55,7 +47,21 @@ module Banzai if name { name: name.tr('"', '') } else - { id: id } + { id: id.to_i } + end + end + + def url_for_object(label, project) + h = Gitlab::Routing.url_helpers + h.namespace_project_issues_url(project.namespace, project, label_name: label.name, + only_path: context[:only_path]) + end + + def object_link_text(object, matches) + if context[:project] == object.project + LabelsHelper.render_colored_label(object) + else + LabelsHelper.render_colored_cross_project_label(object) end end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index ff9887cba1e..504d3df9d34 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,23 +61,21 @@ module Ci @stages = @config[:stages] || @config[:types] @variables = @config[:variables] || {} @cache = @config[:cache] + @jobs = {} + @config.except!(*ALLOWED_YAML_KEYS) + @config.each { |name, param| add_job(name, param) } - # anything that doesn't have script is considered as unknown - @config.each do |name, param| - raise ValidationError, "Unknown parameter: #{name}" unless param.is_a?(Hash) && param.has_key?(:script) - end + raise ValidationError, "Please define at least one job" if @jobs.none? + end - unless @config.values.any?{|job| job.is_a?(Hash)} - raise ValidationError, "Please define at least one job" - end + def add_job(name, job) + return if name.to_s.start_with?('.') - @jobs = {} - @config.each do |key, job| - next if key.to_s.start_with?('.') - stage = job[:stage] || job[:type] || DEFAULT_STAGE - @jobs[key] = { stage: stage }.merge(job) - end + raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) + + stage = job[:stage] || job[:type] || DEFAULT_STAGE + @jobs[name] = { stage: stage }.merge(job) end def build_job(name, job) @@ -112,8 +110,6 @@ module Ci true end - private - def validate_global! unless validate_array_of_strings(@before_script) raise ValidationError, "before_script should be an array of strings" diff --git a/lib/ci/status.rb b/lib/ci/status.rb deleted file mode 100644 index 3fb1fe29494..00000000000 --- a/lib/ci/status.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class Status - def self.get_status(statuses) - if statuses.none? - 'skipped' - elsif statuses.all? { |status| status.success? || status.ignored? } - 'success' - elsif statuses.all?(&:pending?) - 'pending' - elsif statuses.any?(&:running?) || statuses.any?(&:pending?) - 'running' - elsif statuses.all?(&:canceled?) - 'canceled' - else - 'failed' - end - end - end -end diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index f82dce14865..49f702f91f6 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -115,6 +115,15 @@ module Gitlab trans.add_tag(name, value) if trans end + # Sets the action of the current transaction (if any) + # + # action - The name of the action. + def self.action=(action) + trans = current_transaction + + trans.action = action if trans + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index 37232743325..ae85b294d31 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -29,8 +29,8 @@ module Gitlab "in #{GRACE_TIME} seconds" sleep(GRACE_TIME) - Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" - Process.kill('SIGUSR1', Process.pid) + Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}" + Process.kill('SIGTERM', Process.pid) Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\ "#{SHUTDOWN_SIGNAL} to PID #{Process.pid}" diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c54e83339a1..c0a1f45195f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -300,14 +300,6 @@ describe Projects::MergeRequestsController do expect(response.cookies['diff_view']).to eq('parallel') end - - it 'assigns :view param based on cookie' do - request.cookies['diff_view'] = 'parallel' - - go - - expect(controller.params[:view]).to eq 'parallel' - end end describe 'GET commits' do diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index d47e4ab9a4f..ed64e7cf9af 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -46,4 +46,20 @@ describe Projects::ProjectMembersController do end end end + + describe '#index' do + let(:project) { create(:project, :private) } + + context 'when user is member' do + let(:member) { create(:user) } + + before do + project.team << [member, :guest] + sign_in(member) + get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + end + + it { expect(response.status).to eq(200) } + end + end end diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb new file mode 100644 index 00000000000..cf86e2c85e9 --- /dev/null +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe "Dashboard > User filters projects", feature: true do + + describe 'filtering personal projects' do + before do + user = create(:user) + project = create(:project, name: "Victorialand", namespace: user.namespace) + project.team << [user, :master] + + user2 = create(:user) + project2 = create(:project, name: "Treasure", namespace: user2.namespace) + project2.team << [user, :developer] + + login_as(user) + visit dashboard_projects_path + end + + it 'filters by projects "Owned by me"' do + click_link "Owned by me" + + expect(page).to have_css('.is-active', text: 'Owned by me') + expect(page).to have_content('Victorialand') + expect(page).not_to have_content('Treasure') + end + end +end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb new file mode 100644 index 00000000000..7f654684143 --- /dev/null +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -0,0 +1,167 @@ +require 'rails_helper' + +feature 'Issue filtering by Labels', feature: true do + include WaitForAjax + + let(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:label) { create(:label, project: project) } + + before do + bug = create(:label, project: project, title: 'bug') + feature = create(:label, project: project, title: 'feature') + enhancement = create(:label, project: project, title: 'enhancement') + + issue1 = create(:issue, title: "Bugfix1", project: project) + issue1.labels << bug + + issue2 = create(:issue, title: "Bugfix2", project: project) + issue2.labels << bug + issue2.labels << enhancement + + issue3 = create(:issue, title: "Feature1", project: project) + issue3.labels << feature + + project.team << [user, :master] + login_as(user) + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'filter by label bug', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do + expect(page).to have_content "Bugfix1" + expect(page).to have_content "Bugfix2" + end + + it 'should not show "Feature1" in issues list' do + expect(page).not_to have_content "Feature1" + end + + it 'should show label "bug" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "bug" + end + + it 'should not show label "feature" and "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "feature" + expect(find('.filtered-labels')).not_to have_content "enhancement" + end + end + + context 'filter by label feature', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Feature1" in issues list' do + expect(page).to have_content "Feature1" + end + + it 'should not show "Bugfix1" and "Bugfix2" in issues list' do + expect(page).not_to have_content "Bugfix2" + expect(page).not_to have_content "Bugfix1" + end + + it 'should show label "feature" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "feature" + end + + it 'should not show label "bug" and "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "enhancement" + end + end + + context 'filter by label enhancement', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Bugfix2" in issues list' do + expect(page).to have_content "Bugfix2" + end + + it 'should not show "Feature1" and "Bugfix1" in issues list' do + expect(page).not_to have_content "Feature1" + expect(page).not_to have_content "Bugfix1" + end + + it 'should show label "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "enhancement" + end + + it 'should not show label "feature" and "bug" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "feature" + end + end + + context 'filter by label enhancement or feature', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") + execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should not show "Bugfix1" or "Feature1" in issues list' do + expect(page).not_to have_content "Bugfix1" + expect(page).not_to have_content "Feature1" + end + + it 'should show label "enhancement" and "feature" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "enhancement" + expect(find('.filtered-labels')).to have_content "feature" + end + + it 'should not show label "bug" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "bug" + end + end + + context 'filter by label enhancement and bug in issues list', js: true do + before do + page.find('.js-label-select').click + wait_for_ajax + execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()") + execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end + + it 'should show issue "Bugfix2" in issues list' do + expect(page).to have_content "Bugfix2" + end + + it 'should not show "Feature1"' do + expect(page).not_to have_content "Feature1" + end + + it 'should show label "bug" and "enhancement" in filtered-labels' do + expect(find('.filtered-labels')).to have_content "bug" + expect(find('.filtered-labels')).to have_content "enhancement" + end + + it 'should not show label "feature" in filtered-labels' do + expect(find('.filtered-labels')).not_to have_content "feature" + end + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 69b22232f10..192e3619375 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do it 'should filter by any label' do find('.dropdown-menu-labels a', text: 'Any Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + sleep 2 + page.within '.labels-filter' do expect(page).to have_content 'Any Label' end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label') + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label') end it 'should filter by no label' do find('.dropdown-menu-labels a', text: 'No Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + sleep 2 + page.within '.labels-filter' do expect(page).to have_content 'No Label' end @@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do find('.js-label-select').click find('.dropdown-menu-labels .dropdown-content a', text: label.title).click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click sleep 2 end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb new file mode 100644 index 00000000000..5739bc64dfb --- /dev/null +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +feature 'Issue Sidebar', feature: true do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + before do + create(:label, project: project, title: 'bug') + login_as(user) + end + + context 'as a allowed user' do + before do + project.team << [user, :developer] + visit_issue(project, issue) + end + + describe 'when clicking on edit labels', js: true do + it 'dropdown has an option to create a new label' do + find('.block.labels .edit-link').click + + page.within('.block.labels') do + expect(page).to have_content 'Create new' + end + end + end + + context 'creating a new label', js: true do + it 'option to crate a new label is present' do + page.within('.block.labels') do + find('.edit-link').click + + expect(page).to have_content 'Create new' + end + end + + it 'dropdown switches to "create label" section' do + page.within('.block.labels') do + find('.edit-link').click + click_link 'Create new' + + expect(page).to have_content 'Create new label' + end + end + + it 'new label is added' do + page.within('.block.labels') do + find('.edit-link').click + sleep 1 + click_link 'Create new' + + fill_in 'new_label_name', with: 'wontfix' + page.find(".suggest-colors a", match: :first).click + click_button 'Create' + + page.within('.dropdown-page-one') do + expect(page).to have_content 'wontfix' + end + end + end + end + end + + context 'as a guest' do + before do + project.team << [user, :guest] + visit_issue(project, issue) + end + + it 'does not have a option to edit labels' do + expect(page).not_to have_selector('.block.labels .edit-link') + end + end + + def visit_issue(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 35c8f93abc1..b57131f68d5 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -112,7 +112,7 @@ describe 'Issues', feature: true do end describe 'filter issue' do - titles = ['foo','bar','baz'] + titles = %w[foo bar baz] titles.each_with_index do |title, index| let!(title.to_sym) do create(:issue, title: title, @@ -153,8 +153,107 @@ describe 'Issues', feature: true do expect(first_issue).to include('baz') end + describe 'sorting by due date' do + before do + foo.update(due_date: 1.day.from_now) + bar.update(due_date: 6.days.from_now) + end + + it 'sorts by recently due date' do + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_soon) + + expect(first_issue).to include('foo') + end + + it 'sorts by least recently due date' do + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later) + + expect(first_issue).to include('bar') + end + + it 'sorts by least recently due date by excluding nil due dates' do + bar.update(due_date: nil) + + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later) + + expect(first_issue).to include('foo') + end + + context 'with a filter on labels' do + let(:label) { create(:label, project: project) } + before { create(:label_link, label: label, target: foo) } + + it 'sorts by least recently due date by excluding nil due dates' do + bar.update(due_date: nil) + + visit namespace_project_issues_path(project.namespace, project, label_names: [label.name], sort: sort_value_due_date_later) + + expect(first_issue).to include('foo') + end + end + end + + describe 'filtering by due date' do + before do + foo.update(due_date: 1.day.from_now) + bar.update(due_date: 6.days.from_now) + end + + it 'filters by none' do + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::NoDueDate.name) + + expect(page).not_to have_content('foo') + expect(page).not_to have_content('bar') + expect(page).to have_content('baz') + end + + it 'filters by any' do + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::AnyDueDate.name) + + expect(page).to have_content('foo') + expect(page).to have_content('bar') + expect(page).to have_content('baz') + end + + it 'filters by due this week' do + foo.update(due_date: Date.today.beginning_of_week + 2.days) + bar.update(due_date: Date.today.end_of_week) + baz.update(due_date: Date.today - 8.days) + + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisWeek.name) + + expect(page).to have_content('foo') + expect(page).to have_content('bar') + expect(page).not_to have_content('baz') + end + + it 'filters by due this month' do + foo.update(due_date: Date.today.beginning_of_month + 2.days) + bar.update(due_date: Date.today.end_of_month) + baz.update(due_date: Date.today - 50.days) + + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisMonth.name) + + expect(page).to have_content('foo') + expect(page).to have_content('bar') + expect(page).not_to have_content('baz') + end + + it 'filters by overdue' do + foo.update(due_date: Date.today + 2.days) + bar.update(due_date: Date.today + 20.days) + baz.update(due_date: Date.yesterday) + + visit namespace_project_issues_path(project.namespace, project, due_date: Issue::Overdue.name) + + expect(page).not_to have_content('foo') + expect(page).not_to have_content('bar') + expect(page).to have_content('baz') + end + end + describe 'sorting by milestone' do - before :each do + before do foo.milestone = newer_due_milestone foo.save bar.milestone = later_due_milestone @@ -177,7 +276,7 @@ describe 'Issues', feature: true do describe 'combine filter and sort' do let(:user2) { create(:user) } - before :each do + before do foo.assignee = user2 foo.save bar.assignee = user2 @@ -218,13 +317,34 @@ describe 'Issues', feature: true do expect(issue.reload.assignee).to be_nil end + + it 'allows user to select an assignee', js: true do + issue2 = create(:issue, project: project, author: @user) + visit namespace_project_issue_path(project.namespace, project, issue2) + + page.within('.assignee') do + expect(page).to have_content "No assignee" + end + + page.within '.assignee' do + click_link 'Edit' + end + + page.within '.dropdown-menu-user' do + click_link @user.name + end + + page.within('.assignee') do + expect(page).to have_content @user.name + end + end end context 'by unauthorized user' do let(:guest) { create(:user) } - before :each do + before do project.team << [[guest], :guest] end @@ -267,7 +387,7 @@ describe 'Issues', feature: true do context 'by unauthorized user' do let(:guest) { create(:user) } - before :each do + before do project.team << [guest, :guest] issue.milestone = milestone issue.save @@ -285,7 +405,7 @@ describe 'Issues', feature: true do describe 'removing assignee' do let(:user2) { create(:user) } - before :each do + before do issue.assignee = user2 issue.save end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 3d0d0e59fd7..0148c87084a 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -165,7 +165,12 @@ describe 'GitLab Markdown', feature: true do describe 'ExternalLinkFilter' do it 'adds nofollow to external link' do link = doc.at_css('a:contains("Google")') - expect(link.attr('rel')).to match 'nofollow' + expect(link.attr('rel')).to include('nofollow') + end + + it 'adds noreferrer to external link' do + link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to include('noreferrer') end it 'ignores internal link' do diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index c57ab5f3b03..e3ecd60a5f3 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -2,8 +2,14 @@ require 'rails_helper' feature 'Merge Request filtering by Milestone', feature: true do let(:project) { create(:project, :public) } + let!(:user) { create(:user)} let(:milestone) { create(:milestone, project: project) } + before do + project.team << [user, :master] + login_as(user) + end + scenario 'filters by no Milestone', js: true do create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) diff --git a/spec/features/project/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb index 0559b02f321..0559b02f321 100644 --- a/spec/features/project/commits/cherry_pick_spec.rb +++ b/spec/features/projects/commits/cherry_pick_spec.rb diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb new file mode 100644 index 00000000000..c5e3d143d91 --- /dev/null +++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +feature 'Projects > Members > Anonymous user sees members', feature: true do + let(:user) { create(:user) } + let(:group) { create(:group, :public) } + let(:project) { create(:empty_project, :public) } + + background do + project.team << [user, :master] + create(:project_group_link, project: project, group: group) + end + + scenario "anonymous user visits the project's members page and sees the list of members" do + visit namespace_project_project_members_path(project.namespace, project) + + expect(current_path).to eq( + namespace_project_project_members_path(project.namespace, project)) + expect(page).to have_content(user.name) + end +end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 79d5bf4cf06..8625ea6bc10 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -101,12 +101,12 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_denied_for :external } end describe "GET /:project_path/blob" do diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 0a89193eb67..544270b4037 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -101,9 +101,9 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 40daac89d40..4def4f99bc0 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -101,12 +101,12 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } - it { is_expected.to be_denied_for developer } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for :external } end describe "GET /:project_path/builds" do diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 01472743b2a..51b754ff85c 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -13,8 +13,8 @@ feature 'Signup', feature: true do fill_in 'user_password_sign_up', with: user.password click_button "Sign up" - expect(current_path).to eq user_session_path - expect(page).to have_content("A message with a confirmation link has been sent to your email address.") + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb new file mode 100644 index 00000000000..248e004ba6e --- /dev/null +++ b/spec/features/todos/todos_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe 'Dashboard Todos', feature: true do + let(:user) { create(:user) } + let(:author) { create(:user) } + let(:project) { create(:project) } + let(:issue) { create(:issue) } + + describe 'GET /dashboard/todos' do + context 'User does not have todos' do + before do + login_as(user) + visit dashboard_todos_path + end + it 'shows "All done" message' do + expect(page).to have_content "You're all done!" + end + end + + context 'User has a todo', js: true do + before do + create(:todo, :mentioned, user: user, project: project, target: issue, author: author) + login_as(user) + visit dashboard_todos_path + end + + it 'todo is present' do + expect(page).to have_selector('.todos-list .todo', count: 1) + end + + describe 'deleting the todo' do + before do + first('.done-todo').click + end + + it 'is removed from the list' do + expect(page).not_to have_selector('.todos-list .todo') + end + + it 'shows "All done" message' do + expect(page).to have_content("You're all done!") + end + end + end + + context 'User has multiple pages of Todos' do + before do + allow(Todo).to receive(:default_per_page).and_return(1) + + # Create just enough records to cause us to paginate + create_list(:todo, 2, :mentioned, user: user, project: project, target: issue, author: author) + + login_as(user) + end + + it 'is paginated' do + visit dashboard_todos_path + + expect(page).to have_selector('.gl-pagination') + end + + it 'is has the right number of pages' do + visit dashboard_todos_path + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + + describe 'completing last todo from last page', js: true do + it 'redirects to the previous page' do + visit dashboard_todos_path(page: 2) + expect(page).to have_content(Todo.first.body) + + click_link('Done') + + expect(current_path).to eq dashboard_todos_path + expect(page).to have_content(Todo.last.body) + end + end + end + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index b1648055462..bc607a29751 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -62,6 +62,22 @@ describe IssuesFinder do expect(issues).to eq([issue2]) end + it 'returns unique issues when filtering by multiple labels' do + label2 = create(:label, project: project2) + + create(:label_link, label: label2, target: issue2) + + params = { + scope: 'all', + label_name: [label.title, label2.title].join(','), + state: 'opened' + } + + issues = IssuesFinder.new(user, params).execute + + expect(issues).to eq([issue2]) + end + it 'should filter by no label name' do params = { scope: "all", label_name: Label::None.title, state: 'opened' } issues = IssuesFinder.new(user, params).execute diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 4f8d9c67262..f942695b6f0 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -6,8 +6,8 @@ describe CiStatusHelper do let(:success_commit) { double("Ci::Commit", status: 'success') } let(:failed_commit) { double("Ci::Commit", status: 'failed') } - describe 'ci_status_icon' do - it { expect(helper.ci_status_icon(success_commit)).to include('fa-check') } - it { expect(helper.ci_status_icon(failed_commit)).to include('fa-close') } + describe 'ci_icon_for_status' do + it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') } + it { expect(helper.ci_icon_for_status(failed_commit.status)).to include('fa-close') } end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 982c113e84b..b7810185d16 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -11,6 +11,26 @@ describe DiffHelper do let(:diff_refs) { [commit.parent, commit] } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + describe 'diff_view' do + it 'returns a valid value when cookie is set' do + helper.request.cookies[:diff_view] = 'parallel' + + expect(helper.diff_view).to eq 'parallel' + end + + it 'returns a default value when cookie is invalid' do + helper.request.cookies[:diff_view] = 'invalid' + + expect(helper.diff_view).to eq 'inline' + end + + it 'returns a default value when cookie is nil' do + expect(helper.request.cookies).to be_empty + + expect(helper.diff_view).to eq 'inline' + end + end + describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do allow(controller).to receive(:params) { { force_show_diff: true } } diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index c258cfebd73..62389188d2c 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -105,4 +105,30 @@ describe ProjectsHelper do end end end + + describe '#license_short_name' do + let(:project) { create(:project) } + + context 'when project.repository has a license_key' do + it 'returns the nickname of the license if present' do + allow(project.repository).to receive(:license_key).and_return('agpl-3.0') + + expect(helper.license_short_name(project)).to eq('GNU AGPLv3') + end + + it 'returns the name of the license if nickname is not present' do + allow(project.repository).to receive(:license_key).and_return('mit') + + expect(helper.license_short_name(project)).to eq('MIT License') + end + end + + context 'when project.repository has no license_key but a license_blob' do + it 'returns LICENSE' do + allow(project.repository).to receive(:license_key).and_return(nil) + + expect(helper.license_short_name(project)).to eq('LICENSE') + end + end + end end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index e3a8e15330e..f4c5c621bd0 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -24,6 +24,14 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do doc = filter(act) expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to eq 'nofollow' + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + act = %q(<a href="https://google.com/">Google</a>) + doc = filter(act) + + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 94468abcbb3..b0a38e7c251 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -178,27 +178,37 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end describe 'cross project label references' do - let(:another_project) { create(:empty_project, :public) } - let(:project_name) { another_project.name_with_namespace } - let(:label) { create(:label, project: another_project, color: '#00ff00') } - let(:reference) { label.to_reference(project) } + context 'valid project referenced' do + let(:another_project) { create(:empty_project, :public) } + let(:project_name) { another_project.name_with_namespace } + let(:label) { create(:label, project: another_project, color: '#00ff00') } + let(:reference) { label.to_reference(project) } - let!(:result) { reference_filter("See #{reference}") } + let!(:result) { reference_filter("See #{reference}") } - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: label.name) - end + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" + end end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" + context 'project that does not exist referenced' do + let(:result) { reference_filter('aaa/bbb~ccc') } + + it 'does not link reference' do + expect(result.to_html).to eq 'aaa/bbb~ccc' + end end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 643acf0303c..c7ab3185378 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -648,70 +648,131 @@ module Ci end describe "Hidden jobs" do - let(:config) do - YAML.dump({ - '.hidden_job' => { script: 'test' }, - 'normal_job' => { script: 'test' } - }) + let(:config_processor) { GitlabCiYamlProcessor.new(config) } + subject { config_processor.builds_for_stage_and_ref("test", "master") } + + shared_examples 'hidden_job_handling' do + it "doesn't create jobs that start with dot" do + expect(subject.size).to eq(1) + expect(subject.first).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :normal_job, + only: nil, + commands: "test", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + end end - let(:config_processor) { GitlabCiYamlProcessor.new(config) } + context 'when hidden job have a script definition' do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, + 'normal_job' => { script: 'test' } + }) + end - subject { config_processor.builds_for_stage_and_ref("test", "master") } + it_behaves_like 'hidden_job_handling' + end - it "doesn't create jobs that starts with dot" do - expect(subject.size).to eq(1) - expect(subject.first).to eq({ - except: nil, - stage: "test", - stage_idx: 1, - name: :normal_job, - only: nil, - commands: "test", - tag_list: [], - options: {}, - when: "on_success", - allow_failure: false - }) + context "when hidden job doesn't have a script definition" do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' end end describe "YAML Alias/Anchor" do - it "is correctly supported for jobs" do - config = <<EOT + let(:config_processor) { GitlabCiYamlProcessor.new(config) } + subject { config_processor.builds_for_stage_and_ref("build", "master") } + + shared_examples 'job_templates_handling' do + it "is correctly supported for jobs" do + expect(subject.size).to eq(2) + expect(subject.first).to eq({ + except: nil, + stage: "build", + stage_idx: 0, + name: :job1, + only: nil, + commands: "execute-script-for-job", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + expect(subject.second).to eq({ + except: nil, + stage: "build", + stage_idx: 0, + name: :job2, + only: nil, + commands: "execute-script-for-job", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + end + end + + context 'when template is a job' do + let(:config) do + <<EOT job1: &JOBTMPL + stage: build script: execute-script-for-job job2: *JOBTMPL EOT + end - config_processor = GitlabCiYamlProcessor.new(config) + it_behaves_like 'job_templates_handling' + end - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - except: nil, - stage: "test", - stage_idx: 1, - name: :job1, - only: nil, - commands: "execute-script-for-job", - tag_list: [], - options: {}, - when: "on_success", - allow_failure: false - }) - expect(config_processor.builds_for_stage_and_ref("test", "master").second).to eq({ - except: nil, - stage: "test", - stage_idx: 1, - name: :job2, - only: nil, - commands: "execute-script-for-job", - tag_list: [], - options: {}, - when: "on_success", - allow_failure: false - }) + context 'when template is a hidden job' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + script: execute-script-for-job + +job1: *JOBTMPL + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when job adds its own keys to a template definition' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + +job1: + <<: *JOBTMPL + script: execute-script-for-job + +job2: + <<: *JOBTMPL + script: execute-script-for-job +EOT + end + + it_behaves_like 'job_templates_handling' end end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index 329792bb685..b6f7a2e7ec4 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Badge::Build do end context 'build exists' do - let(:ci_commit) { create(:ci_commit, project: project, sha: sha) } + let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } let!(:build) { create(:ci_build, commit: ci_commit) } @@ -57,7 +57,7 @@ describe Gitlab::Badge::Build do describe '#data' do let(:data) { badge.data } - it 'contains infromation about success' do + it 'contains information about success' do expect(status_node(data, 'success')).to be_truthy end end @@ -74,7 +74,7 @@ describe Gitlab::Badge::Build do describe '#data' do let(:data) { badge.data } - it 'contains infromation about failure' do + it 'contains information about failure' do expect(status_node(data, 'failed')).to be_truthy end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 10177c0e8dd..96f7eabbca6 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -123,4 +123,28 @@ describe Gitlab::Metrics do end end end + + describe '.action=' do + context 'without a transaction' do + it 'does nothing' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + not_to receive(:action=) + + Gitlab::Metrics.action = 'foo' + end + end + + context 'with a transaction' do + it 'sets the action of a transaction' do + trans = Gitlab::Metrics::Transaction.new + + expect(Gitlab::Metrics).to receive(:current_transaction). + and_return(trans) + + expect(trans).to receive(:action=).with('foo') + + Gitlab::Metrics.action = 'foo' + end + end + end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 412842337ba..82c18aaa01a 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -27,6 +27,8 @@ describe Ci::Commit, models: true do it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :sha } + it { is_expected.to validate_presence_of :status } + it { is_expected.to delegate_method(:stages).to(:statuses) } it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } @@ -52,57 +54,9 @@ describe Ci::Commit, models: true do it { expect(commit.sha).to start_with(subject) } end - describe :stage do - subject { commit.stage } - - before do - @second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending' - @first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending' - end - - it 'returns first running stage' do - is_expected.to eq('test') - end - - context 'first build succeeded' do - before do - @first.success - end - - it 'returns last running stage' do - is_expected.to eq('deploy') - end - end - - context 'all builds succeeded' do - before do - @first.success - @second.success - end - - it 'returns nil' do - is_expected.to be_nil - end - end - end - describe :create_next_builds do end - describe :refs do - subject { commit.refs } - - before do - FactoryGirl.create :commit_status, commit: commit, name: 'deploy' - FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop' - FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master' - end - - it 'returns all refs' do - is_expected.to contain_exactly('master', 'develop', nil) - end - end - describe :retried do subject { commit.retried } @@ -117,10 +71,10 @@ describe Ci::Commit, models: true do end describe :create_builds do - let!(:commit) { FactoryGirl.create :ci_commit, project: project } + let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false } def create_builds(trigger_request = nil) - commit.create_builds('master', false, nil, trigger_request) + commit.create_builds(nil, trigger_request) end def create_next_builds @@ -143,67 +97,6 @@ describe Ci::Commit, models: true do expect(create_next_builds).to be_falsey end - context 'for different ref' do - def create_develop_builds - commit.create_builds('develop', false, nil, nil) - end - - it 'creates builds' do - expect(create_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(2) - - expect(create_develop_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(4) - expect(commit.refs.size).to eq(2) - expect(commit.builds.pluck(:name).uniq.size).to eq(2) - end - end - - context 'for build triggers' do - let(:trigger) { FactoryGirl.create :ci_trigger, project: project } - let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger } - - it 'creates builds' do - expect(create_builds(trigger_request)).to be_truthy - expect(commit.builds.count(:all)).to eq(2) - end - - it 'rebuilds commit' do - expect(create_builds).to be_truthy - expect(commit.builds.count(:all)).to eq(2) - - expect(create_builds(trigger_request)).to be_truthy - expect(commit.builds.count(:all)).to eq(4) - end - - it 'creates next builds' do - expect(create_builds(trigger_request)).to be_truthy - expect(commit.builds.count(:all)).to eq(2) - commit.builds.update_all(status: "success") - - expect(create_next_builds).to be_truthy - expect(commit.builds.count(:all)).to eq(4) - end - - context 'for [ci skip]' do - before do - allow(commit).to receive(:git_commit_message) { 'message [ci skip]' } - end - - it 'rebuilds commit' do - expect(commit.status).to eq('skipped') - expect(create_builds).to be_truthy - - # since everything in Ci::Commit is cached we need to fetch a new object - new_commit = Ci::Commit.find_by_id(commit.id) - expect(new_commit.status).to eq('pending') - end - end - end - - context 'custom stage with first job allowed to fail' do let(:yaml) do { @@ -284,6 +177,7 @@ describe Ci::Commit, models: true do commit.builds.running_or_pending.each(&:success) expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + commit.reload expect(commit.status).to eq('success') end @@ -306,6 +200,7 @@ describe Ci::Commit, models: true do commit.builds.running_or_pending.each(&:success) expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + commit.reload expect(commit.status).to eq('failed') end @@ -329,6 +224,7 @@ describe Ci::Commit, models: true do expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + commit.reload expect(commit.status).to eq('failed') end @@ -351,6 +247,7 @@ describe Ci::Commit, models: true do commit.builds.running_or_pending.each(&:success) expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + commit.reload expect(commit.status).to eq('failed') end end @@ -402,4 +299,98 @@ describe Ci::Commit, models: true do expect(commit.coverage).to be_nil end end + + describe '#retryable?' do + subject { commit.retryable? } + + context 'no failed builds' do + before do + FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success' + end + + it 'be not retryable' do + is_expected.to be_falsey + end + end + + context 'with failed builds' do + before do + FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running' + FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed' + end + + it 'be retryable' do + is_expected.to be_truthy + end + end + end + + describe '#stages' do + let(:commit2) { FactoryGirl.create :ci_commit, project: project } + subject { CommitStatus.where(commit: [commit, commit2]).stages } + + before do + FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1 + FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0 + end + + it 'return all stages' do + is_expected.to eq(%w(build test)) + end + end + + describe '#update_state' do + it 'execute update_state after touching object' do + expect(commit).to receive(:update_state).and_return(true) + commit.touch + end + + context 'dependent objects' do + let(:commit_status) { build :commit_status, commit: commit } + + it 'execute update_state after saving dependent object' do + expect(commit).to receive(:update_state).and_return(true) + commit_status.save + end + end + + context 'update state' do + let(:current) { Time.now.change(usec: 0) } + let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 } + + before do + build + end + + [:status, :started_at, :finished_at, :duration].each do |param| + it "update #{param}" do + expect(commit.send(param)).to eq(build.send(param)) + end + end + end + end + + describe '#branch?' do + subject { commit.branch? } + + context 'is not a tag' do + before do + commit.tag = false + end + + it 'return true when tag is set to false' do + is_expected.to be_truthy + end + end + + context 'is not a tag' do + before do + commit.tag = true + end + + it 'return false when tag is set to true' do + is_expected.to be_falsey + end + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0e9111c8029..ad47e338a33 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -163,4 +163,12 @@ eos it { expect(commit.reverts_commit?(another_commit)).to be_truthy } end end + + describe '#ci_commits' do + # TODO: kamil + end + + describe '#status' do + # TODO: kamil + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 82c68ff6cb1..971e6750375 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -163,37 +163,73 @@ describe CommitStatus, models: true do end it 'return unique statuses' do - is_expected.to eq([@commit2, @commit3, @commit4, @commit5]) + is_expected.to eq([@commit4, @commit5]) end end - describe :for_ref do - subject { CommitStatus.for_ref('bb').order(:id) } + describe :running_or_pending do + subject { CommitStatus.running_or_pending.order(:id) } before do @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' end - it 'return statuses with equal and nil ref set' do - is_expected.to eq([@commit1]) + it 'return statuses that are running or pending' do + is_expected.to eq([@commit1, @commit2]) end end - describe :running_or_pending do - subject { CommitStatus.running_or_pending.order(:id) } + describe '#before_sha' do + subject { commit_status.before_sha } + + context 'when no before_sha is set for ci::commit' do + before { commit.before_sha = nil } + + it 'return blank sha' do + is_expected.to eq(Gitlab::Git::BLANK_SHA) + end + end + + context 'for before_sha set for ci::commit' do + let(:value) { '1234' } + before { commit.before_sha = value } + + it 'return the set value' do + is_expected.to eq(value) + end + end + end + describe '#stages' do before do - @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' - @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' - @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' + FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success' + FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed' + FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running' + FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success' end - it 'return statuses that are running or pending' do - is_expected.to eq([@commit1, @commit2]) + context 'stages list' do + subject { CommitStatus.where(commit: commit).stages } + + it 'return ordered list of stages' do + is_expected.to eq(%w(build test deploy)) + end + end + + context 'stages with statuses' do + subject { CommitStatus.where(commit: commit).stages_status } + + it 'return list of stages with statuses' do + is_expected.to eq({ + 'build' => 'failed', + 'test' => 'success', + 'deploy' => 'running' + }) + end end end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index b16ccc6e305..4a4cd093435 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -212,4 +212,34 @@ describe Issue, "Issuable" do expect(issue.downvotes).to eq(1) end end + + describe ".with_label" do + let(:project) { create(:project, :public) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:feature) { create(:label, project: project, title: 'feature') } + let(:enhancement) { create(:label, project: project, title: 'enhancement') } + let(:issue1) { create(:issue, title: "Bugfix1", project: project) } + let(:issue2) { create(:issue, title: "Bugfix2", project: project) } + let(:issue3) { create(:issue, title: "Feature1", project: project) } + + before(:each) do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << enhancement + issue3.labels << feature + end + + it 'finds the correct issue containing just enhancement label' do + expect(Issue.with_label(enhancement.title)).to match_array([issue2]) + end + + it 'finds the correct issues containing the same label' do + expect(Issue.with_label(bug.title)).to match_array([issue1, issue2]) + end + + it 'finds the correct issues containing only both labels' do + expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2]) + end + end end diff --git a/spec/lib/ci/status_spec.rb b/spec/models/concerns/statuseable_spec.rb index 47f3df6e3ce..dacbd3034c0 100644 --- a/spec/lib/ci/status_spec.rb +++ b/spec/models/concerns/statuseable_spec.rb @@ -1,8 +1,17 @@ require 'spec_helper' -describe Ci::Status do - describe '.get_status' do - subject { described_class.get_status(statuses) } +describe Statuseable do + before do + @object = Object.new + @object.extend(Statuseable::ClassMethods) + end + + describe '.status' do + before do + allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses)) + end + + subject { @object.status } shared_examples 'build status summary' do context 'all successful' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6f5d912fe5d..d7884cea336 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -404,12 +404,12 @@ describe MergeRequest, models: true do describe 'when the source project exists' do it 'returns the latest commit' do commit = double(:commit, id: '123abc') - ci_commit = double(:ci_commit) + ci_commit = double(:ci_commit, ref: 'master') allow(subject).to receive(:last_commit).and_return(commit) expect(subject.source_project).to receive(:ci_commit). - with('123abc'). + with('123abc', 'master'). and_return(ci_commit) expect(subject.ci_commit).to eq(ci_commit) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f29c389e094..becc743de31 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -441,9 +441,22 @@ describe Project, models: true do describe :ci_commit do let(:project) { create :project } - let(:commit) { create :ci_commit, project: project } + let(:commit) { create :ci_commit, project: project, ref: 'master' } - it { expect(project.ci_commit(commit.sha)).to eq(commit) } + subject { project.ci_commit(commit.sha, 'master') } + + it { is_expected.to eq(commit) } + + context 'return latest' do + let(:commit2) { create :ci_commit, project: project, ref: 'master' } + + before do + commit + commit2 + end + + it { is_expected.to eq(commit2) } + end end describe :builds_enabled do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b561aa663d1..c19524a01f8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -132,7 +132,6 @@ describe Repository, models: true do it { expect(subject.basename).to eq('a/b/c') } end end - end describe '#license_blob' do @@ -148,39 +147,18 @@ describe Repository, models: true do expect(repository.license_blob).to be_nil end - it 'favors license file with no extension' do - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) - repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false) - - expect(repository.license_blob.name).to eq('LICENSE') - end - - it 'favors .md file to .txt' do - repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false) - repository.commit_file(user, 'LICENSE.txt', Licensee::License.new('mit').content, 'Add LICENSE.txt', 'master', false) - - expect(repository.license_blob.name).to eq('LICENSE.md') - end - - it 'favors LICENCE to LICENSE' do - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) - repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false) - - expect(repository.license_blob.name).to eq('LICENCE') - end - - it 'favors LICENSE to COPYING' do - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) - repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false) + it 'detects license file with no recognizable open-source license content' do + repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) expect(repository.license_blob.name).to eq('LICENSE') end - it 'favors LICENCE to COPYING' do - repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false) - repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false) + %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| + it "detects '#{filename}'" do + repository.commit_file(user, filename, Licensee::License.new('mit').content, "Add #{filename}", 'master', false) - expect(repository.license_blob.name).to eq('LICENCE') + expect(repository.license_blob.name).to eq(filename) + end end end @@ -190,8 +168,14 @@ describe Repository, models: true do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end - it 'returns "no-license" when no license is detected' do - expect(repository.license_key).to eq('no-license') + it 'returns nil when no license is detected' do + expect(repository.license_key).to be_nil + end + + it 'detects license file with no recognizable open-source license content' do + repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + + expect(repository.license_key).to be_nil end it 'returns the license key' do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 967c34800d0..5ead735be48 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -59,7 +59,7 @@ describe API::API, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do before do - project.ensure_ci_commit(commit.sha) + project.ensure_ci_commit(commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index 429a24109fd..f3785b19362 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -16,7 +16,8 @@ describe API::CommitStatus, api: true do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:ci_commit) { project.ensure_ci_commit(commit.id) } + let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') } + let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') } it_behaves_like 'a paginated resources' do let(:request) { get api(get_url, reporter) } @@ -25,16 +26,16 @@ describe API::CommitStatus, api: true do context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } - def create_status(opts = {}) - create(:commit_status, { commit: ci_commit }.merge(opts)) + def create_status(commit, opts = {}) + create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts)) end - let!(:status1) { create_status(status: 'running') } - let!(:status2) { create_status(name: 'coverage', status: 'pending') } - let!(:status3) { create_status(ref: 'develop', status: 'running', allow_failure: true) } - let!(:status4) { create_status(name: 'coverage', status: 'success') } - let!(:status5) { create_status(name: 'coverage', ref: 'develop', status: 'success') } - let!(:status6) { create_status(status: 'success') } + let!(:status1) { create_status(master, status: 'running') } + let!(:status2) { create_status(master, name: 'coverage', status: 'pending') } + let!(:status3) { create_status(develop, status: 'running', allow_failure: true) } + let!(:status4) { create_status(master, name: 'coverage', status: 'success') } + let!(:status5) { create_status(develop, name: 'coverage', status: 'success') } + let!(:status6) { create_status(master, status: 'success') } context 'latest commit statuses' do before { get api(get_url, reporter) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 7ff21175c1b..e28998d51b5 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -48,14 +48,14 @@ describe API::API, api: true do expect(response.status).to eq(404) end - it "should return not_found for CI status" do + it "should return nil for commit without CI" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) - expect(json_response['status']).to eq('not_found') + expect(json_response['status']).to be_nil end it "should return status for CI" do - ci_commit = project.ensure_ci_commit(project.repository.commit.sha) + ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) expect(json_response['status']).to eq(ci_commit.status) diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 142b637d291..ffb93bbb120 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -148,14 +148,24 @@ describe API::API, 'ProjectHooks', api: true do expect(response.status).to eq(200) end - it "should return success when deleting non existent hook" do + it "should return a 404 error when deleting non existent hook" do delete api("/projects/#{project.id}/hooks/42", user) - expect(response.status).to eq(200) + expect(response.status).to eq(404) end it "should return a 405 error if hook id not given" do delete api("/projects/#{project.id}/hooks", user) expect(response.status).to eq(405) end + + it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do + test_user = create(:user) + other_project = create(:project) + other_project.team << [test_user, :master] + + delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) + expect(response.status).to eq(404) + expect(WebHook.exists?(hook.id)).to be_truthy + end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index ebd16c7efbe..dfd361a2cdd 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -20,8 +20,8 @@ describe Ci::API::API do describe "POST /builds/register" do it "should start a build" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil) build = commit.builds.first post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -56,8 +56,8 @@ describe Ci::API::API do end it "returns options" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -66,8 +66,8 @@ describe Ci::API::API do end it "returns variables" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -83,10 +83,10 @@ describe Ci::API::API do it "returns variables for triggers" do trigger = FactoryGirl.create(:ci_trigger, project: project) - commit = FactoryGirl.create(:ci_commit, project: project) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) - commit.create_builds('master', false, nil, trigger_request) + commit.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -103,8 +103,8 @@ describe Ci::API::API do end it "returns dependent builds" do - commit = FactoryGirl.create(:ci_commit, project: project) - commit.create_builds('master', false, nil, nil) + commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + commit.create_builds(nil, nil) commit.builds.where(stage: 'test').each(&:success) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb index 1fca3628686..ecc3a88a262 100644 --- a/spec/services/ci/create_builds_service_spec.rb +++ b/spec/services/ci/create_builds_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateBuildsService, services: true do - let(:commit) { create(:ci_commit) } + let(:commit) { create(:ci_commit, ref: 'master') } let(:user) { create(:user) } describe '#execute' do @@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do # subject do - described_class.new.execute(commit, 'test', 'master', nil, user, nil, status) + described_class.new(commit).execute(commit, nil, user, status) end context 'next builds available' do diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 870861ad20a..4cc4b3870d1 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_ci_commit(commit_sha) } + let(:commit) { project.ensure_ci_commit(commit_sha, 'master') } let(:build) { FactoryGirl.create(:ci_build, commit: commit) } describe :execute do diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 6a7ea4b2f44..e91906d0d49 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -100,7 +100,7 @@ describe Issues::BulkUpdateService, services: true do describe :update_milestone do before do - @milestone = create :milestone + @milestone = create(:milestone, project: @project) @params = { issues_ids: [issue.id], milestone_id: @milestone.id diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 5e7915db7e1..ac28b6f71f9 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -3,40 +3,75 @@ require 'spec_helper' describe Issues::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - let(:assignee) { create(:user) } - describe :execute do - context 'valid params' do + describe '#execute' do + let(:issue) { described_class.new(project, user, opts).execute } + + context 'when params are valid' do + let(:assignee) { create(:user) } + let(:milestone) { create(:milestone, project: project) } + let(:labels) { create_pair(:label, project: project) } + before do project.team << [user, :master] project.team << [assignee, :master] + end - opts = { - title: 'Awesome issue', + let(:opts) do + { title: 'Awesome issue', description: 'please fix', - assignee: assignee - } - - @issue = Issues::CreateService.new(project, user, opts).execute + assignee: assignee, + label_ids: labels.map(&:id), + milestone_id: milestone.id } end - it { expect(@issue).to be_valid } - it { expect(@issue.title).to eq('Awesome issue') } - it { expect(@issue.assignee).to eq assignee } + it { expect(issue).to be_valid } + it { expect(issue.title).to eq('Awesome issue') } + it { expect(issue.assignee).to eq assignee } + it { expect(issue.labels).to match_array labels } + it { expect(issue.milestone).to eq milestone } it 'creates a pending todo for new assignee' do attributes = { project: project, author: user, user: assignee, - target_id: @issue.id, - target_type: @issue.class.name, + target_id: issue.id, + target_type: issue.class.name, action: Todo::ASSIGNED, state: :pending } expect(Todo.where(attributes).count).to eq 1 end + + context 'when label belongs to different project' do + let(:label) { create(:label) } + + let(:opts) do + { title: 'Title', + description: 'Description', + label_ids: [label.id] } + end + + it 'does not assign label'do + expect(issue.labels).to_not include label + end + end + + context 'when milestone belongs to different project' do + let(:milestone) { create(:milestone) } + + let(:opts) do + { title: 'Title', + description: 'Description', + milestone_id: milestone.id } + end + + it 'does not assign milestone' do + expect(issue.milestone).to_not eq milestone + end + end end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 6b214a0d96b..52f69306994 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -4,10 +4,15 @@ describe Issues::UpdateService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } - let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) } - let(:label) { create(:label) } + let(:project) { create(:empty_project) } + let(:label) { create(:label, project: project) } let(:label2) { create(:label) } - let(:project) { issue.project } + + let(:issue) do + create(:issue, title: 'Old title', + assignee_id: user3.id, + project: project) + end before do project.team << [user, :master] diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index cb8cff2fa8c..213e8c2eb3a 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1,14 +1,19 @@ require 'spec_helper' describe MergeRequests::UpdateService, services: true do + let(:project) { create(:project) } let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } - let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) } - let(:project) { merge_request.project } - let(:label) { create(:label) } + let(:label) { create(:label, project: project) } let(:label2) { create(:label) } + let(:merge_request) do + create(:merge_request, :simple, title: 'Old title', + assignee_id: user3.id, + source_project: project) + end + before do project.team << [user, :master] project.team << [user2, :developer] |