diff options
author | James Lopez <james@jameslopez.es> | 2016-04-07 15:15:48 +0300 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2016-04-07 15:15:48 +0300 |
commit | e8b7e37c6de2c541510ff804730e3d9946857b9e (patch) | |
tree | 6553e5dd3424c52ae0261f24f6671f585b01bb54 | |
parent | 05be0c306ea8663461ee73023f162aeaf77a4325 (diff) | |
parent | b30ebdaa1a704f4e81e91153b1b33a4c1c1a5c12 (diff) |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into fix/project-import_url
109 files changed, 1794 insertions, 627 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 53f115c92c8..1dc49ca336d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,6 @@ image: "ruby:2.1" services: - mysql:latest - - postgres:latest - redis:latest cache: @@ -35,134 +34,86 @@ spec:feature: script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - tags: - - ruby - - mysql spec:api: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - tags: - - ruby - - mysql spec:models: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - tags: - - ruby - - mysql spec:lib: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - tags: - - ruby - - mysql spec:services: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - tags: - - ruby - - mysql spec:other: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other - tags: - - ruby - - mysql spinach:project:half: stage: test script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - tags: - - ruby - - mysql spinach:project:rest: stage: test script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - tags: - - ruby - - mysql spinach:other: stage: test script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - tags: - - ruby - - mysql teaspoon: stage: test script: - RAILS_ENV=test bundle exec teaspoon - tags: - - ruby - - mysql rubocop: stage: test script: - bundle exec rubocop - tags: - - ruby - - mysql scss-lint: stage: test script: - bundle exec rake scss_lint - tags: - - ruby brakeman: stage: test script: - bundle exec rake brakeman - tags: - - ruby - - mysql flog: stage: test script: - bundle exec rake flog - tags: - - ruby - - mysql flay: stage: test script: - bundle exec rake flay - tags: - - ruby - - mysql bundler:audit: stage: test only: - master script: - - "bundle exec bundle-audit update" - - "bundle exec bundle-audit check --ignore OSVDB-115941" - tags: - - ruby - - mysql + - "bundle exec bundle-audit check --update --ignore OSVDB-115941" # Ruby 2.2 jobs @@ -178,9 +129,6 @@ spec:feature:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:api:ruby22: stage: test @@ -193,9 +141,6 @@ spec:api:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:models:ruby22: stage: test @@ -208,9 +153,6 @@ spec:models:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:lib:ruby22: stage: test @@ -223,9 +165,6 @@ spec:lib:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:services:ruby22: stage: test @@ -238,9 +177,6 @@ spec:services:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:other:ruby22: stage: test @@ -253,9 +189,6 @@ spec:other:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spinach:project:half:ruby22: stage: test @@ -269,9 +202,6 @@ spinach:project:half:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spinach:project:rest:ruby22: stage: test @@ -285,9 +215,6 @@ spinach:project:rest:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spinach:other:ruby22: stage: test @@ -301,10 +228,6 @@ spinach:other:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql - notify:slack: stage: notifications diff --git a/.scss-lint.yml b/.scss-lint.yml index 3ce0c4901bd..835a4a88c44 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -7,21 +7,44 @@ exclude: - 'app/assets/stylesheets/pages/emojis.scss' linters: + # Reports when you use improper spacing around ! (the "bang") in !default, + # !global, !important, and !optional flags. BangFormat: enabled: false + # Whether or not to prefer `border: 0` over `border: none`. BorderZero: enabled: false + # Reports when you define a rule set using a selector with chained classes + # (a.k.a. adjoining classes). + ChainedClasses: + enabled: false + + # Prefer hexadecimal color codes over color keywords. + # (e.g. `color: green` is a color keyword) ColorKeyword: enabled: false + # Prefer color literals (keywords or hexadecimal codes) to be used only in + # variable declarations. They should be referred to via variables everywhere + # else. ColorVariable: enabled: false + # Which form of comments to prefer in CSS. Comment: enabled: false + + # Reports @debug statements (which you probably left behind accidentally). + DebugStatement: + enabled: false + # Rule sets should be ordered as follows: + # - @extend declarations + # - @include declarations without inner @content + # - properties, @include declarations with inner @content + # - nested rule sets. DeclarationOrder: enabled: false @@ -32,15 +55,25 @@ linters: DisableLinterReason: enabled: true + # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: false + # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: enabled: false + # Reports when you have an empty rule set. EmptyRule: enabled: false + # Reports when you have an @extend directive. + ExtendDirective: + enabled: false + + # Files should always have a final newline. This results in better diffs + # when adding lines to the file, since SCM systems such as git won't + # think that you touched the last line. FinalNewline: enabled: false @@ -53,12 +86,17 @@ linters: HexNotation: enabled: true + # Avoid using ID selectors. IdSelector: enabled: false + # The basenames of @imported SCSS partials should not begin with an + # underscore and should not include the filename extension. ImportPath: enabled: false + # Avoid using !important in properties. It is usually indicative of a + # misunderstanding of CSS specificity and can lead to brittle code. ImportantRule: enabled: false @@ -67,33 +105,51 @@ linters: enabled: true width: 2 + # Don't write leading zeros for numeric values with a decimal point. LeadingZero: enabled: false + # Reports when you define the same selector twice in a single sheet. MergeableSelector: enabled: false + # Functions, mixins, variables, and placeholders should be declared + # with all lowercase letters and hyphens instead of underscores. NameFormat: enabled: false + # Avoid nesting selectors too deeply. NestingDepth: enabled: false + # Always use placeholder selectors in @extend. PlaceholderInExtend: enabled: false + # Sort properties in a strict order. PropertySortOrder: enabled: false + # Reports when you use an unknown or disabled CSS property + # (ignoring vendor-prefixed properties). PropertySpelling: enabled: false + # Configure which units are allowed for property values. + PropertyUnits: + enabled: false + + # Pseudo-elements, like ::before, and ::first-letter, should be declared + # with two colons. Pseudo-classes, like :hover and :first-child, should + # be declared with one colon. PseudoElement: enabled: false + # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: enabled: false + # Don't write selectors with a depth of applicability greater than 3. SelectorDepth: enabled: false @@ -113,9 +169,12 @@ linters: enabled: true allow_single_line_rule_sets: true + # Split selectors onto separate lines after each comma, and have each + # individual selector occupy a single line. SingleLinePerSelector: enabled: false + # Commas in lists should be followed by a space. SpaceAfterComma: enabled: false @@ -128,29 +187,75 @@ linters: # colon. SpaceAfterPropertyName: enabled: true + + # Variables should be formatted with a single space separating the colon + # from the variable's value. + SpaceAfterVariableColon: + enabled: false + + # Variables should be formatted with no space between the name and the + # colon. + SpaceAfterVariableName: + enabled: false + # Operators should be formatted with a single space on both sides of an + # infix operator. SpaceAroundOperator: enabled: false # Opening braces should be preceded by a single space. SpaceBeforeBrace: enabled: true + + # Parentheses should not be padded with spaces. + SpaceBetweenParens: + enabled: false + # Enforces that string literals should be written with a consistent form + # of quotes (single or double). StringQuotes: enabled: false + # Property values, @extend, @include, and @import directives, and variable + # declarations should always end with a semicolon. TrailingSemicolon: enabled: false + # Reports lines containing trailing whitespace. TrailingWhitespace: enabled: false + # Don't write trailing zeros for numeric values with a decimal point. + TrailingZero: + enabled: false + + # Don't use the `all` keyword to specify transition properties. + TransitionAll: + enabled: false + + # Numeric values should not contain unnecessary fractional portions. UnnecessaryMantissa: enabled: false + # Do not use parent selector references (&) when they would otherwise + # be unnecessary. UnnecessaryParentReference: enabled: false + + # URLs should be valid and not contain protocols or domain names. + UrlFormat: + enabled: false + + # URLs should always be enclosed within quotes. + UrlQuotes: + enabled: false + + # Properties, like color and font, are easier to read and maintain + # when defined using variables rather than literals. + VariableForProperty: + enabled: false + # Avoid vendor prefixes. Or rather: don't write them yourself. VendorPrefix: enabled: false diff --git a/CHANGELOG b/CHANGELOG index 39239bebcfb..1d1e541e65f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,23 +2,37 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - 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) - Improved Markdown rendering performance !3389 (Yorick Peterse) - - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) + - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) + - Expose project badges in project settings - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API + - Fix Error 500 after renaming a project path (Stan Hu) - Fix avatar stretching by providing a cropping feature + - Allow SAML to handle external users based on user's information !3530 - Add endpoints to archive or unarchive a project !3372 - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) - Add default scope to projects to exclude projects pending deletion + - Ensure empty recipients are rejected in BuildsEmailService + - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Fix creation of merge requests for orphaned branches (Stan Hu) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) + - Improved UX of the navigation sidebar + - Fix admin/projects when using visibility levels on search (PotHix) + - Build status notifications + - API: Expose user location (Robert Schilling) + +v 8.6.5 (unreleased) + - Only update repository language if it is not set to improve performance + - Check permissions when user attempts to import members from another project v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) @@ -138,6 +152,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.9 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + v 8.5.8 - Bump Git version requirement to 2.7.4 @@ -279,6 +296,12 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.7 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + +v 8.4.6 + - Bump Git version requirement to 2.7.4 + v 8.4.5 - No CE-specific changes @@ -392,6 +415,12 @@ 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.6 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + +v 8.3.5 + - Bump Git version requirement to 2.7.4 + v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) @@ -290,7 +290,7 @@ group :development, :test do gem 'rubocop', '~> 0.38.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'coveralls', '~> 0.8.2', require: false - gem 'simplecov', '~> 0.10.0', require: false + gem 'simplecov', '~> 0.11.0', require: false gem 'flog', require: false gem 'flay', require: false gem 'bundler-audit', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 229089f431d..1ba8d748db1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,7 +99,7 @@ GEM bullet (5.0.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.9.0) - bundler-audit (0.4.0) + bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) @@ -136,10 +136,9 @@ GEM colorize (0.7.7) concurrent-ruby (1.0.0) connection_pool (2.2.0) - coveralls (0.8.9) + coveralls (0.8.13) json (~> 1.8) - rest-client (>= 1.6.8, < 2) - simplecov (~> 0.10.0) + simplecov (~> 0.11.0) term-ansicolor (~> 1.3) thor (~> 0.19.1) tins (~> 1.6.0) @@ -176,8 +175,6 @@ GEM diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) - domain_name (0.5.25) - unf (>= 0.0.5, < 1.0.0) doorkeeper (2.2.2) railties (>= 3.2) dropzonejs-rails (0.7.2) @@ -421,8 +418,6 @@ GEM nokogiri (~> 1.6.0) ruby_parser (~> 3.5) htmlentities (4.3.4) - http-cookie (1.0.2) - domain_name (~> 0.5) http_parser.rb (0.5.3) httparty (0.13.7) json (~> 1.8) @@ -480,7 +475,6 @@ GEM nested_form (0.3.2) net-ldap (0.12.1) net-ssh (3.0.1) - netrc (0.11.0) newrelic_rpm (3.14.1.311) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) @@ -657,10 +651,6 @@ GEM listen (~> 3.0) responders (2.1.1) railties (>= 4.2.0, < 5.1) - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) rinku (1.7.3) rotp (2.1.1) rouge (1.10.1) @@ -754,7 +744,7 @@ GEM rufus-scheduler (>= 2.0.24) sidekiq (>= 4.0.0) simple_oauth (0.1.9) - simplecov (0.10.0) + simplecov (0.11.2) docile (~> 1.1.0) json (~> 1.8) simplecov-html (~> 0.10.0) @@ -845,7 +835,7 @@ GEM underscore-rails (1.8.3) unf (0.1.4) unf_ext - unf_ext (0.0.7.1) + unf_ext (0.0.7.2) unicode-display_width (1.0.2) unicorn (4.9.0) kgio (~> 2.6) @@ -1032,7 +1022,7 @@ DEPENDENCIES shoulda-matchers (~> 2.8.0) sidekiq (~> 4.0) sidekiq-cron (~> 0.4.0) - simplecov (~> 0.10.0) + simplecov (~> 0.11.0) sinatra (~> 1.4.4) six (~> 0.2.0) slack-notifier (~> 1.2.0) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4f032a82e58..e8d25591f63 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -1,5 +1,6 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] + ARROW_KEY_CODES = [38, 40] HAS_VALUE_CLASS = "has-value" constructor: (@input, @options) -> @@ -22,19 +23,23 @@ class GitLabDropdownFilter # Key events timeout = "" @input.on "keyup", (e) => + keyCode = e.which + + return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 + if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS $inputContainer.addClass HAS_VALUE_CLASS else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS $inputContainer.removeClass HAS_VALUE_CLASS - if e.keyCode is 13 and @input.val() isnt "" + if keyCode is 13 and @input.val() isnt "" if @options.enterCallback @options.enterCallback() return clearTimeout timeout timeout = setTimeout => - blur_field = @shouldBlur e.keyCode + blur_field = @shouldBlur keyCode search_text = @input.val() if blur_field and @filterInputBlur @@ -96,6 +101,7 @@ class GitLabDropdown LOADING_CLASS = "is-loading" PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + currentIndex = -1 FILTER_INPUT = '.dropdown-input .dropdown-input-field' @@ -145,11 +151,11 @@ class GitLabDropdown data: => return @fullData callback: (data) => + currentIndex = -1 @parseData data - @highlightRow 1 enterCallback: => if @enterCallback - @selectFirstRow() + @selectRowAtIndex 0 # Event listeners @@ -171,10 +177,11 @@ class GitLabDropdown selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> - selected = self.rowClicked $(@) + $el = $(@) + selected = self.rowClicked $el if self.options.clicked - self.options.clicked(selected) + self.options.clicked(selected, $el, e) # Finds an element inside wrapper element getElement: (selector) -> @@ -218,6 +225,8 @@ class GitLabDropdown return true opened: => + @addArrowKeyEvent() + contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" @remote.execute() @@ -228,6 +237,7 @@ class GitLabDropdown @dropdown.trigger('shown.gl.dropdown') hidden: (e) => + @removeArrayKeyEvent() if @options.filterable @dropdown .find(".dropdown-input-field") @@ -307,11 +317,11 @@ class GitLabDropdown if @highlight text = @highlightTextMatches(text, @filterInput.val()) - html = "<li>" - html += "<a href='#{url}' class='#{cssClass}'>" - html += text - html += "</a>" - html += "</li>" + html = "<li> + <a href='#{url}' class='#{cssClass}'> + #{text} + </a> + </li>" return html @@ -322,11 +332,11 @@ class GitLabDropdown ).join('') noResults: -> - html = "<li>" - html += "<a class='dropdown-menu-empty-link is-focused'>" - html += "No matching results." - html += "</a>" - html += "</li>" + html = "<li class='dropdown-menu-empty-link'> + <a href='#' class='is-focused'> + No matching results. + </a> + </li>" highlightRow: (index) -> if @filterInput.val() isnt "" @@ -351,6 +361,8 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + else + selectedObject else if !value? field.remove() @@ -366,7 +378,7 @@ class GitLabDropdown if @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) if value? - if !field.length + if !field.length and fieldName # Create hidden input for form input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" if @options.inputId? @@ -378,16 +390,81 @@ class GitLabDropdown return selectedObject - selectFirstRow: -> - selector = '.dropdown-content li:first-child a' + selectRowAtIndex: (index) -> + selector = ".dropdown-content li:not(.divider):eq(#{index}) a" + if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content li:first-child a" + selector = ".dropdown-page-one #{selector}" # simulate a click on the first link - $(selector).trigger "click" + $(selector, @dropdown).trigger "click" + + addArrowKeyEvent: -> + ARROW_KEY_CODES = [38, 40] + $input = @dropdown.find(".dropdown-input-field") + + selector = '.dropdown-content li:not(.divider)' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one #{selector}" + + $('body').on 'keydown', (e) => + currentKeyCode = e.which + + if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0 + e.preventDefault() + e.stopImmediatePropagation() + + PREV_INDEX = currentIndex + $listItems = $(selector, @dropdown) + + # if @options.filterable + # $input.blur() + + if currentKeyCode is 40 + # Move down + currentIndex += 1 if currentIndex < ($listItems.length - 1) + else if currentKeyCode is 38 + # Move up + currentIndex -= 1 if currentIndex > 0 + + @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX + + return false + + if currentKeyCode is 13 + @selectRowAtIndex currentIndex + + removeArrayKeyEvent: -> + $('body').off 'keydown' + + highlightRowAtIndex: ($listItems, index) -> + # Remove the class for the previously focused row + $('.is-focused', @dropdown).removeClass 'is-focused' + + # Update the class for the row at the specific index + $listItem = $listItems.eq(index) + $listItem.find('a:first-child').addClass "is-focused" + + # Dropdown content scroll area + $dropdownContent = $listItem.closest('.dropdown-content') + dropdownScrollTop = $dropdownContent.scrollTop() + dropdownContentHeight = $dropdownContent.outerHeight() + dropdownContentTop = $dropdownContent.prop('offsetTop') + dropdownContentBottom = dropdownContentTop + dropdownContentHeight + + # Get the offset bottom of the list item + listItemHeight = $listItem.outerHeight() + listItemTop = $listItem.prop('offsetTop') + listItemBottom = listItemTop + listItemHeight + + if listItemBottom > dropdownContentBottom + dropdownScrollTop + # Scroll the dropdown content down + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom) + else if listItemTop < dropdownContentTop + dropdownScrollTop + # Scroll the dropdown content up + $dropdownContent.scrollTop(listItemTop - dropdownContentTop) $.fn.glDropdown = (opts) -> return @.each -> if (!$.data @, 'glDropdown') $.data(@, 'glDropdown', new GitLabDropdown @, opts) - diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index d663e34871c..946d83b7bdd 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -6,25 +6,10 @@ class @Issue constructor: -> # Prevent duplicate event bindings @disableTaskList() - @fixAffixScroll() if $('a.btn-close').length @initTaskList() @initIssueBtnEventListeners() - fixAffixScroll: -> - fixAffix = -> - $discussion = $('.issuable-discussion') - $sidebar = $('.issuable-sidebar') - if $sidebar.hasClass('no-affix') - $sidebar.removeClass(['affix-top','affix']) - discussionHeight = $discussion.height() - sidebarHeight = $sidebar.height() - if sidebarHeight > discussionHeight - $discussion.height(sidebarHeight + 50) - $sidebar.addClass('no-affix') - $(window).on('resize', fixAffix) - fixAffix() - initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList @@ -49,7 +34,7 @@ class @Issue issueStatus = if isClose then 'close' else 'open' new Flash(issueFailMessage, 'alert') success: (data, textStatus, jqXHR) -> - if data.saved + if 'id' of data $(document).trigger('issuable:change'); if isClose $('a.btn-close').addClass('hidden') diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index b1479bfb449..0d9f2094c2a 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -26,6 +26,20 @@ $(".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 @@ -54,6 +68,7 @@ # 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: -> diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee new file mode 100644 index 00000000000..abd556e0b4e --- /dev/null +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -0,0 +1,31 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.getUrlParameter = (sParam) -> + sPageURL = decodeURIComponent(window.location.search.substring(1)) + sURLVariables = sPageURL.split('&') + sParameterName = undefined + 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] + i++ + + # # + # @param {Object} params - url keys and value to merge + # @param {String} url + # # + w.gl.utils.mergeUrlParams = (params, url) -> + newUrl = decodeURIComponent(url) + for paramName, paramValue of params + pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" + if url.search(pattern) >= 0 + newUrl = newUrl.replace pattern, "$1#{paramValue}$2" + else + newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" + newUrl + +) window diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 6af5a48a0bb..1f46e331427 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -15,8 +15,6 @@ class @MergeRequest this.$('.show-all-commits').on 'click', => this.showAllCommits() - @fixAffixScroll(); - @initTabs() # Prevent duplicate event bindings @@ -30,20 +28,6 @@ class @MergeRequest $: (selector) -> this.$el.find(selector) - fixAffixScroll: -> - fixAffix = -> - $discussion = $('.issuable-discussion') - $sidebar = $('.issuable-sidebar') - if $sidebar.hasClass('no-affix') - $sidebar.removeClass(['affix-top','affix']) - discussionHeight = $discussion.height() - sidebarHeight = $sidebar.height() - if sidebarHeight > discussionHeight - $discussion.height(sidebarHeight + 50) - $sidebar.addClass('no-affix') - $(window).on('resize', fixAffix) - fixAffix() - initTabs: -> if @opts.action != 'new' # `MergeRequests#new` has no tab-persisting or lazy-loading behavior diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 7102a0673e9..84a8887fbce 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -15,6 +15,8 @@ class @MergeRequestWidget @pollCIStatus() notifyPermissions() + setOpts: (@opts) -> + mergeInProgress: (deleteSourceBranch = false)-> $.ajax type: 'GET' @@ -48,7 +50,7 @@ class @MergeRequestWidget @getCIStatus(true) @readyForCICheck = false - ), 5000 + ), 10000 getCIStatus: (showNotification) -> _this = @ @@ -61,6 +63,10 @@ class @MergeRequestWidget @firstCICheck = false @opts.ci_status = data.status + if @opts.ci_status is '' + @opts.ci_status = data.status + return + if data.status isnt @opts.ci_status @showCIStatus data.status if data.coverage diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ff06c57f2b5..86e3b860fcb 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -251,13 +251,11 @@ class @Notes Sets some hidden fields in the form. ### setupMainTargetNoteForm: -> - # find the form form = $(".js-new-note-form") - # insert the form after the button - form.clone().replaceAll $(".js-main-target-form") - form = form.prev("form") + # Set a global clone of the form for later cloning + @formClone = form.clone() # show the form @setupNoteForm(form) @@ -266,9 +264,7 @@ class @Notes form.removeClass "js-new-note-form" form.addClass "js-main-target-form" - # remove unnecessary fields and buttons form.find("#note_line_code").remove() - form.find(".js-close-discussion-note-form").remove() ### General note form setup. @@ -297,7 +293,14 @@ class @Notes else previewButton.removeClass("turn-on").addClass "turn-off" + textarea.on 'focus', -> + $(this).closest('.md-area').addClass 'is-focused' + + textarea.on 'blur', -> + $(this).closest('.md-area').removeClass 'is-focused' + autosize(textarea) + new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -307,7 +310,6 @@ class @Notes ] # remove notify commit author checkbox for non-commit notes - form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit" GitLab.GfmAutoComplete.setup() new DropzoneInput(form) form.show() @@ -455,15 +457,15 @@ class @Notes Shows the note form below the notes. ### replyToDiscussionNote: (e) => - form = $(".js-new-note-form") + form = @formClone.clone() replyLink = $(e.target).closest(".js-discussion-reply-button") replyLink.hide() # insert the form after the button - form.clone().insertAfter replyLink + replyLink.after form # show the form - @setupDiscussionNoteForm(replyLink, replyLink.next("form")) + @setupDiscussionNoteForm(replyLink, form) ### Shows the diff or discussion form and does some setup on it. @@ -488,7 +490,9 @@ class @Notes .text(form.find('.js-close-discussion-note-form').data('cancel-text')) @setupNoteForm form form.find(".js-note-text").focus() - form.addClass "js-discussion-note-form" + form + .removeClass('js-main-target-form') + .addClass("discussion-form js-discussion-note-form") ### Called when clicking on the "add a comment" button on the side of a diff line. @@ -498,9 +502,8 @@ class @Notes ### addDiffNote: (e) => e.preventDefault() - link = e.currentTarget - form = $(".js-new-note-form") - row = $(link).closest("tr") + $link = $(e.currentTarget) + row = $link.closest("tr") nextRow = row.next() hasNotes = nextRow.is(".notes_holder") addForm = false @@ -509,7 +512,7 @@ class @Notes # In parallel view, look inside the correct left/right pane if @isParallelView() - lineType = $(link).data("lineType") + lineType = $link.data("lineType") targetContent += "." + lineType rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>" @@ -531,11 +534,11 @@ class @Notes addForm = true if addForm - newForm = form.clone() + newForm = @formClone.clone() newForm.appendTo row.next().find(targetContent) # show the form - @setupDiscussionNoteForm $(link), newForm + @setupDiscussionNoteForm $link, newForm ### Called in response to "cancel" on a diff note form. @@ -560,7 +563,6 @@ class @Notes cancelDiscussionForm: (e) => e.preventDefault() - form = $(".js-new-note-form") form = $(e.target).closest(".js-discussion-note-form") @removeDiscussionNoteForm(form) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 030655491bf..6a7b4ad1db7 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -62,6 +62,8 @@ class @SearchAutocomplete search: fields: ['text'] data: @getData.bind(@) + selectable: true + clicked: @onClick.bind(@) getData: (term, callback) -> _this = @ @@ -102,6 +104,8 @@ class @SearchAutocomplete lastCategory = suggestion.category data.push + id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}" + category: suggestion.category text: suggestion.label url: suggestion.url @@ -133,12 +137,19 @@ class @SearchAutocomplete } bindEvents: -> + $(document).on 'click', @onDocumentClick @searchInput.on 'keydown', @onSearchInputKeyDown @searchInput.on 'keyup', @onSearchInputKeyUp @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus - @searchInput.on 'blur', @onSearchInputBlur - @clearInput.on 'click', @onRemoveLocationClick + @clearInput.on 'click', @onClearInputClick + + onDocumentClick: (e) => + # If clicking outside the search box + # And search input is not focused + # And we are not clicking inside a suggestion + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + @onSearchInputBlur() enableAutocomplete: -> # No need to enable anything if user is not logged in @@ -181,6 +192,8 @@ class @SearchAutocomplete # We should display the menu only when input is not empty @enableAutocomplete() + @wrap.toggleClass 'has-value', !!e.target.value + # Avoid falsy value to be returned return @@ -189,27 +202,20 @@ class @SearchAutocomplete e.stopImmediatePropagation() onSearchInputFocus: => + @isFocused = true @wrap.addClass('search-active') - onRemoveLocationClick: (e) => + onClearInputClick: (e) => e.preventDefault() - @removeLocationBadge() @searchInput.val('').focus() - @skipBlurEvent = true onSearchInputBlur: (e) => - @skipBlurEvent = false - - # We should wait to make sure we are not clearing the input instead - setTimeout( => - return if @skipBlurEvent + @isFocused = false + @wrap.removeClass('search-active') - @wrap.removeClass('search-active') - - # If input is blank then restore state - if @searchInput.val() is '' - @restoreOriginalState() - , 150) + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() addLocationBadge: (item) -> category = if item.category? then "#{item.category}: " else '' @@ -268,3 +274,23 @@ class @SearchAutocomplete <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>" @dropdownContent.html(html) + + onClick: (item, $el, e) -> + if location.pathname.indexOf(item.url) isnt -1 + e.preventDefault() + if not @badgePresent + if item.category is 'Projects' + @projectInputEl.val(item.id) + @addLocationBadge( + value: 'This project' + ) + + if item.category is 'Groups' + @groupInputEl.val(item.id) + @addLocationBadge( + value: 'This group' + ) + + $el.removeClass('is-active') + @disableAutocomplete() + @searchInput.val('').focus() diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index e1c5446eaac..99f35ecfb0f 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -42,7 +42,7 @@ class @ZenMode $(e.currentTarget).trigger('zen_mode:leave') $(document).on 'zen_mode:enter', (e) => - @enter(e.target.parentNode) + @enter($(e.target).closest('.md-area').find('.zen-backdrop')) $(document).on 'zen_mode:leave', (e) => @exit() diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index db1a8b1bf78..2ade341c9dd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -125,13 +125,6 @@ p.time { height: 150px; } -// Fixes alignment on notes. -.new_note { - label { - text-align: left; - } -} - // Fix issue with notes & lists creating a bunch of bottom borders. li.note { img { max-width: 100% } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index a26ace5cc19..b15f4e7bd5e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -20,6 +20,7 @@ margin: 0; text-align: left; padding: 10px $gl-padding; + word-wrap: break-word; .file-actions { float: right; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 8328aac4e7a..c8f86d60e3b 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,9 +1,7 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - padding: 0; - border: 0; - margin-bottom: 5px; + margin-bottom: -5px; .div-dropzone-focus { border-color: #66afe9 !important; @@ -25,12 +23,10 @@ .div-dropzone-spinner { position: absolute; - top: 100%; - left: 100%; - margin-top: -1.1em; - margin-left: -1.1em; + bottom: 10px; + right: 5px; opacity: 0; - font-size: 30px; + font-size: 20px; transition: opacity 200ms ease-in-out; } @@ -65,17 +61,30 @@ position: relative; } +.md-header { + .nav-links { + .active { + a { + border-bottom-color: #000; + } + } + + a { + padding-top: 0; + line-height: 1; + } + } +} + .referenced-users { color: #4c4e54; padding-top: 10px; } .md-preview-holder { - background: #fff; - border: 1px solid #ddd; - min-height: 169px; - padding: 5px; - box-shadow: none; + min-height: 167px; + padding: 10px 0; + overflow-x: auto; } .markdown-area { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index c3c7bc9fdbe..7b2aada5a0d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -250,7 +250,7 @@ a > code { * Textareas intended for GFM * */ -textarea.js-gfm-input { +.js-gfm-input { font-family: $monospace_font; color: $gl-text-color; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 98fe794d362..8d3ad934a50 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80); $orange-normal: #e75e40; $orange-dark: #ce5237; -$red-light: #f06559; -$red-normal: #e52c5a; -$red-dark: #d22852; +$red-light: #e52c5a; +$red-normal: #d22852; +$red-dark: darken($red-normal, 5%); $border-white-light: #f1f2f4; $border-white-normal: #d6dae2; @@ -128,9 +128,9 @@ $border-orange-light: #fc6d26; $border-orange-normal: #ce5237; $border-orange-dark: #c14e35; -$border-red-light: #f24f41; -$border-red-normal: #d22852; -$border-red-dark: #ca264f; +$border-red-light: #d22852; +$border-red-normal: #ca264f; +$border-red-dark: darken($border-red-normal, 5%); $help-well-bg: #fafafa; $help-well-border: #e5e5e5; @@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc; /* * Search Box */ -$search-input-border-color: $dropdown-input-focus-border; +$search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; -$search-input-width: $dropdown-width; +$search-input-width: 244px; $location-badge-color: #aaa; $location-badge-bg: $gray-normal; +$location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; -$location-active-color: $gl-text-color; -$location-active-bg: $search-input-border-color; +$location-icon-active-color: #807e7e; /* * Notes @@ -217,3 +217,9 @@ $notes-light-color: #8e8e8e; $notes-action-color: #c3c3c3; $notes-role-color: #8e8e8e; $notes-role-border-color: #e4e4e4; + +$note-disabled-comment-color: #b2b2b2; +$note-form-border-color: #e5e5e5; +$note-toolbar-color: #959494; + +$zen-control-hover-color: #111; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 02e24ec7c4d..f870ea0d87f 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,61 +1,62 @@ -.zennable { - a.js-zen-enter { - color: $gl-gray; - position: absolute; +.zen-backdrop { + &.fullscreen { + background-color: white; + position: fixed; top: 0; - right: 4px; - line-height: 56px; - } + bottom: 0; + left: 0; + right: 0; + z-index: 1031; - a.js-zen-leave { - display: none; - color: $gl-text-color; - position: absolute; - top: 10px; - right: 10px; - padding: 5px; - font-size: 36px; + textarea { + border: none; + box-shadow: none; + border-radius: 0; + color: #000; + font-size: 20px; + line-height: 26px; + padding: 30px; + display: block; + outline: none; + resize: none; + height: 100vh; + max-width: 900px; + margin: 0 auto; + } - &:hover { - color: #111; + .zen-control-leave { + display: block; + position: absolute; + top: 0; } } +} - .zen-backdrop { - &.fullscreen { - background-color: white; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 1031; +.zen-cotrol { + padding: 0; + color: #555; + background: none; + border: 0; +} - textarea { - border: none; - box-shadow: none; - border-radius: 0; - color: #000; - font-size: 20px; - line-height: 26px; - padding: 30px; - display: block; - outline: none; - resize: none; - height: 100vh; - max-width: 900px; - margin: 0 auto; - } +.zen-control-full { + color: $note-toolbar-color; - a.js-zen-enter { - display: none; - } + &:hover { + color: $gl-link-color; + text-decoration: none; + } +} - a.js-zen-leave { - display: block; - position: absolute; - top: 0; - } - } +.zen-control-leave { + display: none; + color: $gl-text-color; + position: absolute; + right: 10px; + padding: 5px; + font-size: 36px; + + &:hover { + color: $zen-control-hover-color; } } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index d3eda1a57e6..5917f089720 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -33,8 +33,12 @@ .description { margin-top: 6px; - p:last-child { - margin-bottom: 0; + p { + overflow-x: auto; + + &:last-child { + margin-bottom: 0; + } } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index f1368d74b3b..7a12aa96476 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -59,10 +59,15 @@ border-collapse: separate; margin: 0; padding: 0; + .line_holder td { line-height: $code_line_height; font-size: $code_font_size; } + + td { + white-space: nowrap; + } } tr.line_holder.parallel { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7ff63ca20b6..1c6a4208974 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -195,42 +195,6 @@ line-height: 31px; } -.disabled-comment-area { - padding: 16px 0; - - .disabled-profile { - width: 40px; - height: 40px; - background: $border-gray-dark; - border-radius: 20px; - display: inline-block; - margin-right: 10px; - } - - .disabled-comment { - background: $gray-light; - display: inline-block; - vertical-align: top; - height: 200px; - border-radius: 4px; - border: 1px solid $border-gray-normal; - padding-top: 90px; - text-align: center; - right: 20px; - position: absolute; - left: 70px; - margin-bottom: 20px; - - span { - color: #b2b2b2; - - a { - color: $md-link-color; - } - } - } -} - .builds { .table-holder { overflow-x: scroll; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 655f88b0c2c..a909776b437 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -1,10 +1,6 @@ /** * Note Form */ - -.comment-btn { - @extend .btn-create; -} .reply-btn { @extend .btn-primary; margin: 10px $gl-padding; @@ -17,16 +13,17 @@ } .diff-file, .discussion { - .new_note { + .new-note { margin: 0; border: none; } } -.new_note { + +.new-note { display: none; } -.new_note, .note-edit-form { +.new-note, .note-edit-form { .note-form-actions { margin-top: $gl-padding; } @@ -40,21 +37,18 @@ img { max-width: 100%; } +} - .note_text { - width: 100%; - } +.note-textarea { + padding: 10px 0; + font-family: $regular_font; + border: 0; - .comment-hints { - margin-top: -12px; + &:focus { + outline: 0; } } -/* loading indicator */ -.notes-busy { - margin: 18px; -} - .note-image-attach { @extend .col-md-4; margin-left: 45px; @@ -62,38 +56,29 @@ } .common-note-form { - margin: 0; - background: #fff; - padding: $gl-padding; - margin-left: -$gl-padding; - margin-right: -$gl-padding; - margin-bottom: -$gl-padding; -} - -.note-form-actions { - .note-form-option { - margin-top: 8px; - margin-left: 30px; - @extend .pull-left; - } - - .js-notify-commit-author { - float: left; - } - - .write-preview-btn { - // makes the "absolute" position for links relative to this - position: relative; - - // preview/edit buttons - > a { - position: absolute; - right: 5px; - top: 8px; + .md-area { + padding: $gl-padding-top $gl-padding; + border: 1px solid $note-form-border-color; + border-radius: $border-radius-base; + + &.is-focused { + border-color: $focus-border-color; + box-shadow: 0 0 2px rgba(#000, .2), + 0 0 4px rgba($focus-border-color, .4); + + .comment-toolbar, + .nav-links { + border-color: $focus-border-color; + } } } } +.discussion-form { + padding: $gl-padding-top $gl-padding; + background-color: #fff; +} + .note-edit-form { display: none; font-size: 15px; @@ -152,11 +137,49 @@ } } -.comment-hints { - color: #999; - background: #fff; - padding: 7px; - margin-top: -7px; - border: 1px solid $border-color; - font-size: 13px; +.comment-toolbar { + padding-top: $gl-padding-top; + color: $note-toolbar-color; + border-top: 1px solid $border-color; +} + +.toolbar-button { + padding: 0; + background: none; + border: 0; + font-size: 14px; + line-height: 16px; + + &:hover, + &:focus { + color: $gl-link-color; + outline: 0; + } + + @media (min-width: $screen-md-min) { + float: left; + margin-right: $gl-padding; + + &:last-child { + float: right; + margin-right: 0; + } + } +} + +.toolbar-button-icon { + position: relative; + top: 1px; + margin-right: 3px; + color: inherit; + font-size: 16px; +} + +.toolbar-text { + font-size: 14px; + line-height: 16px; + + @media (min-width: $screen-md-min) { + float: left; + } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 92fcaaeeacf..aca86457c70 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -20,6 +20,12 @@ ul.notes { .timeline-content { margin-left: 55px; + + &.timeline-content-form { + @media (max-width: $screen-sm-max) { + margin-left: 0; + } + } } .note-created-ago, .note-updated-at { @@ -76,7 +82,7 @@ ul.notes { // On diffs code should wrap nicely and not overflow pre { code { - white-space: pre-wrap; + white-space: pre; } } @@ -149,7 +155,7 @@ ul.notes { &.notes_content { background-color: #fff; border-width: 1px 0; - padding-top: 0; + padding: 0; vertical-align: top; &.parallel { border-width: 1px; @@ -281,3 +287,21 @@ ul.notes { } } } + +.disabled-comment { + margin-left: -$gl-padding-top; + margin-right: -$gl-padding-top; + background-color: $gray-light; + border-radius: $border-radius-base; + border: 1px solid $border-gray-normal; + color: $note-disabled-comment-color; + line-height: 200px; + + .disabled-comment-text { + line-height: normal; + } + + a { + color: $gl-link-color; + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 4e6aa8cd1a6..fcca9d4faf5 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -315,7 +315,7 @@ pre.light-well { } .git-empty { - margin: 0 7px; + margin: 0 7px 7px; h5 { color: #5c5d5e; @@ -401,7 +401,7 @@ pre.light-well { } .commit_short_id { - margin-right: 5px; + margin: 0 5px; color: $gl-link-color; font-weight: 600; } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 3c74d25beb0..f0f3744c6fa 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -135,25 +135,25 @@ .location-badge { @include transition(all .15s); - background-color: $location-active-bg; + background-color: $location-badge-active-bg; color: $white-light; } .search-input-wrap { i { - color: $location-active-color; + color: $location-icon-active-color; } } + } - &.has-location-badge { - .search-icon { - display: none; - } + &.has-value { + .search-icon { + display: none; + } - .clear-icon { - cursor: pointer; - display: block; - } + .clear-icon { + cursor: pointer; + display: block; } } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 5e5e38a0ba6..dbb6daf0d70 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,4 +1,4 @@ -.container-fluid .content { +.container-fluid { .ci-status { padding: 2px 7px; margin-right: 5px; diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 4089091d569..c6b3105544a 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController def index @projects = Project.all @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? + @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.non_archived unless params[:with_archived].present? diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 21135f7d607..d28e96c3f18 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end else saml_user = Gitlab::Saml::User.new(oauth) - saml_user.save + saml_user.save if saml_user.changed? @user = saml_user.gl_user continue_login_process diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 657ee94cfd7..74150ad606b 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -68,7 +68,9 @@ class Projects::ApplicationController < ApplicationController end def require_non_empty_project - redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo? + # Be sure to return status code 303 to avoid a double DELETE: + # http://api.rubyonrails.org/classes/ActionController/Redirecting.html + redirect_to namespace_project_path(@project.namespace, @project), status: 303 if @project.empty_repo? end def require_branch_head diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 6d4d4360988..824aa41db51 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -1,5 +1,12 @@ class Projects::BadgesController < Projects::ApplicationController - before_action :no_cache_headers + layout 'project_settings' + before_action :authorize_admin_project!, only: [:index] + before_action :no_cache_headers, except: [:index] + + def index + @ref = params[:ref] || @project.default_branch || 'master' + @build_badge = Gitlab::Badge::Build.new(@project, @ref) + end def build badge = Gitlab::Badge::Build.new(project, params[:ref]) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index c0a53734921..d09e7375b67 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html do redirect_to namespace_project_branches_path(@project.namespace, - @project) + @project), status: 303 end format.js { render status: status[:return_code] } end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e7bddc4a6f1..e457db2f0b7 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def apply_import - giver = Project.find(params[:source_project_id]) - status = @project.team.import(giver, current_user) - notice = status ? "Successfully imported" : "Import failed" + source_project = Project.find(params[:source_project_id]) + + if can?(current_user, :read_project_member, source_project) + status = @project.team.import(source_project, current_user) + notice = status ? "Successfully imported" : "Import failed" + else + return render_404 + end redirect_to(namespace_project_project_members_path(project.namespace, project), notice: notice) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 00df1c9c965..d79f16e6a5a 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController namespace_project_find_file_path(@project.namespace, @project, @id) when "graphs_commits" commits_namespace_project_graph_path(@project.namespace, @project, @id) + when "badges" + namespace_project_badges_path(@project.namespace, @project, ref: @id) else namespace_project_commits_path(@project.namespace, @project, @id) end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 02ceb8f4334..9f3a4a69721 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController ) end + def markdown_preview + text = params[:text] + + ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) + ext.analyze(text) + + render json: { + body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), + references: { + users: ext.users.map(&:username) + } + } + end + def git_access end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8c3a74c8236..3cc37e59855 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController def update status = ::Projects::UpdateService.new(@project, current_user, project_params).execute + # Refresh the repo in case anything changed + @repository = project.repository + respond_to do |format| if status flash[:notice] = "Project '#{@project.name}' was successfully updated." diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 65677a3dd3c..c29f4609e93 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -5,7 +5,8 @@ class SessionsController < Devise::SessionsController skip_before_action :check_2fa_requirement, only: [:destroy] prepend_before_action :check_initial_setup, only: [:new] - prepend_before_action :authenticate_with_two_factor, only: [:create] + prepend_before_action :authenticate_with_two_factor, + if: :two_factor_enabled?, only: [:create] prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] @@ -56,10 +57,10 @@ class SessionsController < Devise::SessionsController end def find_user - if user_params[:login] - User.by_login(user_params[:login]) - elsif user_params[:otp_attempt] && session[:otp_user_id] + if session[:otp_user_id] User.find(session[:otp_user_id]) + elsif user_params[:login] + User.by_login(user_params[:login]) end end @@ -83,11 +84,13 @@ class SessionsController < Devise::SessionsController end end + def two_factor_enabled? + find_user.try(:two_factor_enabled?) + end + def authenticate_with_two_factor user = self.resource = find_user - return unless user && user.two_factor_enabled? - if user_params[:otp_attempt].present? && session[:otp_user_id] if valid_otp_attempt?(user) # Remove any lingering user data from login diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index f6313255cbb..f9f04838766 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -50,12 +50,15 @@ class BuildsEmailService < Service def execute(push_data) return unless supported_events.include?(push_data[:object_kind]) + return unless should_build_be_notified?(push_data) - if should_build_be_notified?(push_data) + recipients = all_recipients(push_data) + + if recipients.any? BuildEmailWorker.perform_async( push_data[:build_id], - all_recipients(push_data), - push_data, + recipients, + push_data ) end end @@ -84,7 +87,7 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = recipients.split(',') + all_recipients = recipients.split(',').compact.reject(&:blank?) if add_pusher? && data[:user][:email] all_recipients << "#{data[:user][:email]}" diff --git a/app/models/repository.rb b/app/models/repository.rb index e80c2238402..8dead3a5884 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -331,6 +331,8 @@ class Repository # Runs code after a repository has been created. def after_create expire_exists_cache + expire_root_ref_cache + expire_emptiness_caches end # Runs code just before a repository is deleted. @@ -364,6 +366,11 @@ class Repository expire_tag_count_cache end + def before_import + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been forked/imported. def after_import expire_emptiness_caches diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c007d648dd6..dc74c02760b 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -43,23 +43,27 @@ class GitPushService < BaseService @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) process_commit_messages end - # Checks if the main language has changed in the project and if so - # it updates it accordingly - update_main_language # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. update_merge_requests + # Checks if the main language has changed in the project and if so + # it updates it accordingly + update_main_language + perform_housekeeping end def update_main_language - current_language = @project.repository.main_language + # Performance can be bad so for now only check main_language once + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937 + return if @project.main_language.present? - unless current_language == @project.main_language - return @project.update_attributes(main_language: current_language) - end + return unless is_default_branch? + return unless push_to_new_branch? || push_to_existing_branch? + current_language = @project.repository.main_language + @project.update_attributes(main_language: current_language) true end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 2015897dd19..ef15ef6a473 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -46,6 +46,8 @@ module Projects def import_data return unless has_importer? + project.repository.before_import + unless importer.execute raise Error, 'The remote data could not be imported.' end diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 9d4ab9847a8..6b208c3d0bb 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ -- if controller.controller_path =~ /^groups/ +- if controller.controller_path =~ /^groups/ && @group.persisted? - label = 'This group' -- if controller.controller_path =~ /^projects/ +- if controller.controller_path =~ /^projects/ && @project.persisted? - label = 'This project' .search.search-form{class: "#{'has-location-badge' if label.present?}"} @@ -21,8 +21,8 @@ %a.is-focused.dropdown-menu-empty-link Loading... = dropdown_loading - %i.search-icon - %i.clear-icon.js-clear-input + %i.search-icon + %i.clear-icon.js-clear-input = hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index dc3050f02e5..d429a928464 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -51,8 +51,13 @@ = icon('code fw') %span Variables - = nav_link path: 'triggers#index' do + = nav_link(controller: :triggers) do = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do = icon('retweet fw') %span Triggers + = nav_link(controller: :badges) do + = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do + = icon('star-half-empty fw') + %span + Badges diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index ab527e8e438..a7ef31acd3d 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -5,10 +5,14 @@ - content_for :scripts_body_top do - project = @target_project || @project + - if @project_wiki + - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project) + - else + - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - if current_user :javascript window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; - window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}"; + window.markdown_preview_path = "#{markdown_preview_path}"; - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 1fb37ef6621..4920910fee1 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,18 +1,19 @@ .md-area - .md-header.clearfix + .md-header %ul.nav-links %li.active - %a.js-md-write-button(href="#md-write-holder" tabindex="-1") + %a.js-md-write-button{ href: "#md-write-holder" } Write %li - %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") + %a.js-md-preview-button{ href: "#md-preview-holder" } Preview + %li.pull-right + %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' } + Go full screen - %div - .md-write-holder - = yield - .md.md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} + .md-write-holder + = yield + .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))} - if defined?(referenced_users) && referenced_users %div.referenced-users.hide diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e701253d7de..bddff5cdcbc 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,12 +1,8 @@ -.zennable - .zen-backdrop - - classes << ' js-gfm-input js-autosize markdown-area' - - if defined?(f) && f - = f.text_area attr, class: classes - - else - = text_area_tag attr, nil, class: classes - %a.js-zen-enter(tabindex="-1" href="#") - = icon('expand') - Edit in fullscreen - %a.js-zen-leave(tabindex="-1" href="#") - = icon('compress') +.zen-backdrop + - classes << ' js-gfm-input js-autosize markdown-area' + - if defined?(f) && f + = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + - else + = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + = icon('compress') diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml new file mode 100644 index 00000000000..c22384ddf46 --- /dev/null +++ b/app/views/projects/badges/index.html.haml @@ -0,0 +1,24 @@ +- page_title 'Badges' +- badges_path = namespace_project_badges_path(@project.namespace, @project) +- header_title project_title(@project, 'Badges', badges_path) + +.prepend-top-10 + .panel.panel-default + .panel-heading + %b Builds badge · + = @build_badge.to_html + .pull-right + = render 'shared/ref_switcher', destination: 'badges' + .panel-body + .row + .col-md-2.text-center + Markdown + .col-md-10.code.js-syntax-highlight + = highlight('.md', @build_badge.to_markdown) + .row + %hr + .row + .col-md-2.text-center + HTML + .col-md-10.code.js-syntax-highlight + = highlight('.html', @build_badge.to_html) diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 2be06aebe6c..92d95358937 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -22,4 +22,6 @@ if(typeof merge_request_widget === 'undefined') { merge_request_widget = new MergeRequestWidget(opts); + } else { + merge_request_widget.setOpts(opts); } diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 2999befffc6..23e4f93eab5 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,10 +1,11 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f| + = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' = render 'projects/notes/hints' .note-form-actions.clearfix = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' - = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel' + %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } + Cancel diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index f675f092da1..c446ecec2c3 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 6e7929bdab0..0b002043408 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,9 +1,8 @@ -.comment-hints.clearfix - .pull-left +.comment-toolbar.clearfix + .toolbar-text + Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 - tip: - = random_markdown_tip - .pull-right - = link_to '#', class: 'markdown-selector', tabindex: -1 do - = icon('paperclip') - Attach a file + is supported + %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } + = icon('file-image-o', class: 'toolbar-button-icon') + Attach a file diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 34fe1743f4b..a681d6dece4 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -18,7 +18,7 @@ = access = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil-square-o') - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do = icon('trash-o') .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 910eb6cf66e..cc42aab5c52 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,20 +1,21 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" -.js-notes-busy - -.js-main-target-form -- if can? current_user, :create_note, @project - = render "projects/notes/form", view: diff_view -- else - .disabled-comment-area - .disabled-profile - .disabled-comment - %span - Please - = link_to "register",new_user_session_path - or - = link_to "login",new_user_session_path - to post a comment +%ul.notes.timeline + %li.timeline-entry + - if can? current_user, :create_note, @project + .timeline-icon.hidden-xs.hidden-sm + %a.author_link{ href: user_path(current_user) } + = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' + .timeline-content.timeline-content-form + = render "projects/notes/form", view: diff_view + - else + .disabled-comment.text-center + .disabled-comment-text.inline + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index fb1c3476f65..ddee467b14b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -345,6 +345,8 @@ production: &base # # - { name: 'saml', # label: 'Our SAML Provider', + # groups_attribute: 'Groups', + # external_groups: ['Contractors', 'Freelancers'], # args: { # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', @@ -352,6 +354,7 @@ production: &base # issuer: 'https://gitlab.example.com', # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' # } } + # # - { name: 'crowd', # args: { # crowd_server_url: 'CROWD SERVER URL', diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index a44316bc3a4..b9176688bc4 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -3,5 +3,6 @@ Premailer::Rails.config.merge!( generate_text_part: false, preserve_styles: true, remove_comments: true, - remove_ids: true + remove_ids: true, + remove_scripts: false ) diff --git a/config/routes.rb b/config/routes.rb index 6bf22fb4456..842fbb99843 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -575,6 +575,7 @@ Rails.application.routes.draw do # Order matters to give priority to these matches get '/wikis/git_access', to: 'wikis#git_access' get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' + post '/wikis/markdown_preview', to:'wikis#markdown_preview' post '/wikis', to: 'wikis#create' get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID @@ -749,10 +750,11 @@ Rails.application.routes.draw do end resources :runner_projects, only: [:create, :destroy] - resources :badges, only: [], path: 'badges/*ref', - constraints: { ref: Gitlab::Regex.git_reference_regex } do + resources :badges, only: [:index] do collection do - get :build, constraints: { format: /svg/ } + scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do + get :build, constraints: { format: /svg/ } + end end end end diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 237700bbcd9..10096779844 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -261,13 +261,13 @@ tree and traverse it. - Run the following check command to make sure that the LDAP settings are correct and GitLab can see your users: - ```bash - # For Omnibus installations - sudo gitlab-rake gitlab:ldap:check + ```bash + # For Omnibus installations + sudo gitlab-rake gitlab:ldap:check - # For installations from source - sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production - ``` + # For installations from source + sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production + ``` ### Connection Refused diff --git a/doc/api/milestones.md b/doc/api/milestones.md index a6828728264..e4202025f80 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -7,8 +7,24 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones GET /projects/:id/milestones?iid=42 +GET /projects/:id/milestones?state=active +GET /projects/:id/milestones?state=closed ``` +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `iid` | integer | optional | Return only the milestone having the given `iid` | +| `state` | string | optional | Return only `active` or `closed` milestones` | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones +``` + +Example Response: + ```json [ { @@ -25,10 +41,6 @@ GET /projects/:id/milestones?iid=42 ] ``` -Parameters: - -- `id` (required) - The ID of a project -- `iid` (optional) - Return the milestone having the given `iid` ## Get single milestone diff --git a/doc/api/users.md b/doc/api/users.md index 383e7c76ab0..7d2b4897cff 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -69,6 +69,7 @@ GET /users "state": "blocked", "created_at": "2012-05-23T08:01:01Z", "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", @@ -126,6 +127,7 @@ Parameters: "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", @@ -154,6 +156,7 @@ Parameters: "confirmed_at": "2012-05-23T08:00:58Z", "last_sign_in_at": "2015-03-23T08:00:58Z", "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", @@ -191,6 +194,7 @@ Parameters: - `extern_uid` (optional) - External UID - `provider` (optional) - External provider name - `bio` (optional) - User's biography +- `location` (optional) - User's location - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `confirm` (optional) - Require confirmation - true (default) or false @@ -218,6 +222,7 @@ Parameters: - `extern_uid` - External UID - `provider` - External provider name - `bio` - User's biography +- `location` (optional) - User's location - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) @@ -260,6 +265,7 @@ GET /user "state": "active", "created_at": "2012-05-23T08:00:58Z", "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4316f3c1f64..7da9b31e30d 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -38,7 +38,7 @@ services: - postgres before_script: - - bundle_install + - bundle install stages: - build diff --git a/doc/development/README.md b/doc/development/README.md index 1b281809afc..8940b558fb6 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -4,6 +4,7 @@ - [CI setup](ci_setup.md) for testing GitLab - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) +- [Instrumentation](instrumentation.md) - [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Rake tasks](rake_tasks.md) for development - [Shell commands](shell_commands.md) in the GitLab codebase diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md new file mode 100644 index 00000000000..c0192bd6709 --- /dev/null +++ b/doc/development/instrumentation.md @@ -0,0 +1,37 @@ +# Instrumenting Ruby Code + +GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby +code. This can be used to measure the time spent in a specific part of a larger +chunk of code. The resulting data is written to a separate series. + +To start measuring a block of Ruby code you should use +`Gitlab::Metrics.measure` and give it a name for the series to store the data +in: + +```ruby +Gitlab::Metrics.measure(:user_logins) do + ... +end +``` + +The first argument of this method is the series name and should be plural. This +name will be prefixed with `rails_` or `sidekiq_` depending on whether the code +was run in the Rails application or one of the Sidekiq workers. In the +above example the final series names would be as follows: + +- rails_user_logins +- sidekiq_user_logins + +Series names should be plural as this keeps the naming style in line with the +other series names. + +By default metrics measured using a block contain a single value, "duration", +which contains the number of milliseconds it took to execute the block. Custom +values can be added by passing a Hash as the 2nd argument. Custom tags can be +added by passing a Hash as the 3rd argument. A simple example is as follows: + +```ruby +Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do + ... +end +``` diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index fb20308c49c..30f0c15dacc 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,3 +1,3 @@ # GitLab LDAP integration -This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md). +This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 1c3dc707f6d..8a7205caaa4 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -131,8 +131,75 @@ On the sign in page there should now be a SAML button below the regular sign in Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. +## External Groups + +>**Note:** +This setting is only available on GitLab 8.7 and above. + +SAML login includes support for external groups. You can define in the SAML +settings which groups, to which your users belong in your IdP, you wish to be +marked as [external](../permissions/permissions.md). + +### Requirements + +First you need to tell GitLab where to look for group information. For this you +need to make sure that your IdP server sends a specific `AttributeStament` along +with the regular SAML response. Here is an example: + +```xml +<saml:AttributeStatement> + <saml:Attribute Name="Groups"> + <saml:AttributeValue xsi:type="xs:string">SecurityGroup</saml:AttributeValue> + <saml:AttributeValue xsi:type="xs:string">Developers</saml:AttributeValue> + <saml:AttributeValue xsi:type="xs:string">Designers</saml:AttributeValue> + </saml:Attribute> +</saml:AttributeStatement> +``` + +The name of the attribute can be anything you like, but it must contain the groups +to which a user belongs. In order to tell GitLab where to find these groups, you need +to add a `groups_attribute:` element to your SAML settings. You will also need to +tell GitLab which groups are external via the `external_groups:` element: + +```yaml +{ name: 'saml', + label: 'Our SAML Provider', + groups_attribute: 'Groups', + external_groups: ['Freelancers', 'Interns'], + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } } +``` + ## Customization +### `auto_sign_in_with_provider` + +You can add this setting to your GitLab configuration to automatically redirect you +to your SAML server for authentication, thus removing the need to click a button +before actually signing in. + +For omnibus package: + +```ruby +gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml' +``` + +For installations from source: + +```yaml +omniauth: + auto_sign_in_with_provider: saml +``` + +Please keep in mind that every sign in attempt will be redirected to the SAML server, +so you will not be able to sign in using local credentials. Make sure that at least one +of the SAML users has admin permissions. + ### `attribute_statements` >**Note:** @@ -205,6 +272,10 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th where it can then be seen in the usual logs, or as a flash message in the login screen. +That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers` +for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for +installations from source. + ### Invalid audience This error means that the IdP doesn't recognize GitLab as a valid sender and diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e6eb1cf3819..4f199b6af6f 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -31,7 +31,7 @@ _GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ -For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. +GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). You can use GFM in @@ -47,10 +47,10 @@ You can also use other rich text files in GitLab. You might have to install a de GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. Line-breaks, or softreturns, are rendered if you end a line with two or more spaces - Roses are red [followed by two or more spaces] + Roses are red [followed by two or more spaces] Violets are blue Sugar is sweet @@ -67,7 +67,7 @@ It is not reasonable to italicize just _part_ of a word, especially when you're perform_complicated_task do_this_and_do_that_and_another_thing -perform_complicated_task +perform_complicated_task do_this_and_do_that_and_another_thing ## URL auto-linking diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 3d375e47c8e..6219693b8a8 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md). | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | -| Force push to protected branches | | | | | | -| Remove protected branches | | | | | | +| Force push to protected branches [^2] | | | | | | +| Remove protected branches [^2] | | | | | | [^1]: If **Allow guest to access builds** is enabled in CI settings +[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner ## Group diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 906b66a4a63..32c3e99f450 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -125,7 +125,7 @@ module SharedDiffNote step 'I should only see one diff form' do page.within(diff_file_selector) do - expect(page).to have_css("form.new_note", count: 1) + expect(page).to have_css("form.new-note", count: 1) end end @@ -161,7 +161,7 @@ module SharedDiffNote step 'I should see a temporary diff comment form' do page.within(diff_file_selector) do - expect(page).to have_css(".js-temp-notes-holder form.new_note") + expect(page).to have_css(".js-temp-notes-holder form.new-note") end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index fb0462d6e04..a3c3887ab46 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -2,7 +2,7 @@ module SharedNote include Spinach::DSL step 'I delete a comment' do - page.within('.notes') do + page.within('.main-notes-list') do find('.note').hover find(".js-note-delete").click end @@ -128,7 +128,7 @@ module SharedNote end step 'I edit the last comment with a +1' do - page.within(".notes") do + page.within(".main-notes-list") do find(".note").hover find('.js-note-edit').click end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 592100a7045..231840148d9 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -64,7 +64,7 @@ module API authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) - not_found!("Branch does not exist") unless @branch + not_found!("Branch") unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch.destroy if protected_branch diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 340fc5452ab..4c49442bf8b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -15,7 +15,7 @@ module API class User < UserBasic expose :created_at expose :is_admin?, as: :is_admin - expose :bio, :skype, :linkedin, :twitter, :website_url + expose :bio, :location, :skype, :linkedin, :twitter, :website_url end class Identity < Grape::Entity diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index c5cd73943fb..afb6ffa3609 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -3,17 +3,33 @@ module API class Milestones < Grape::API before { authenticate! } + helpers do + def filter_milestones_state(milestones, state) + case state + when 'active' then milestones.active + when 'closed' then milestones.closed + else milestones + end + end + end + resource :projects do # Get a list of project milestones # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project + # state (optional) - Return "active" or "closed" milestones # Example Request: # GET /projects/:id/milestones + # GET /projects/:id/milestones?state=active + # GET /projects/:id/milestones?state=closed get ":id/milestones" do authorize! :read_milestone, user_project - present paginate(user_project.milestones), with: Entities::Milestone + milestones = user_project.milestones + milestones = filter_milestones_state(milestones, params[:state]) + + present paginate(milestones), with: Entities::Milestone end # Get a single project milestone diff --git a/lib/api/users.rb b/lib/api/users.rb index 13ab17c6904..0a14bac07c0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -58,6 +58,7 @@ module API # extern_uid - External authentication provider UID # provider - External provider # bio - Bio + # location - Location of the user # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false # confirm - Require user confirmation - true (default) or false @@ -67,7 +68,7 @@ module API post do authenticated_as_admin! required_attributes! [:email, :password, :name, :username] - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external] admin = attrs.delete(:admin) confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) user = User.build_user(attrs) @@ -106,6 +107,7 @@ module API # website_url - Website url # projects_limit - Limit projects each user can create # bio - Bio + # location - Location of the user # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false # external - Flags the user as external - true or false(default) @@ -114,7 +116,7 @@ module API put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external] user = User.find(params[:id]) not_found!('User') unless user diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index f21dbef216c..b8962379cb5 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -119,7 +119,7 @@ module Banzai elsif element_node?(node) yield_valid_link(node) do |link, text| - if ref_pattern && link =~ /\A#{ref_pattern}/ + if ref_pattern && link =~ /\A#{ref_pattern}\z/ replace_link_node_with_href(node, link) do object_link_filter(link, ref_pattern, link_text: text) end diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index 7ce26db1b90..d08267a9d6c 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -118,7 +118,7 @@ module Banzai end if path - content_tag(:img, nil, src: path) + content_tag(:img, nil, src: path, class: 'gfm') end end @@ -144,12 +144,18 @@ module Banzai # if it is not. def process_page_link_tag(parts) if parts.size == 1 - url = parts[0].strip + reference = parts[0].strip else - name, url = *parts.compact.map(&:strip) + name, reference = *parts.compact.map(&:strip) end - content_tag(:a, name || url, href: url) + if url?(reference) + href = reference + else + href = ::File.join(project_wiki_base_path, reference) + end + + content_tag(:a, name || reference, href: href, class: 'gfm') end def project_wiki diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb new file mode 100644 index 00000000000..06d10c98501 --- /dev/null +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -0,0 +1,56 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that "fixes" relative links to files in a repository. + # + # Context options: + # :project_wiki + class WikiLinkFilter < HTML::Pipeline::Filter + + def call + return doc unless project_wiki? + + doc.search('a:not(.gfm)').each do |el| + process_link_attr el.attribute('href') + end + + doc + end + + protected + + def project_wiki? + !context[:project_wiki].nil? + end + + def process_link_attr(html_attr) + return if html_attr.blank? || file_reference?(html_attr) + + uri = URI(html_attr.value) + if uri.relative? && uri.path.present? + html_attr.value = rebuild_wiki_uri(uri).to_s + end + rescue URI::Error + # noop + end + + def rebuild_wiki_uri(uri) + uri.path = ::File.join(project_wiki_base_path, uri.path) + uri + end + + def file_reference?(html_attr) + !File.extname(html_attr.value).blank? + end + + def project_wiki + context[:project_wiki] + end + + def project_wiki_base_path + project_wiki && project_wiki.wiki_base_path + end + end + end +end diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb index 0b5a9e0b2b8..c37b8e71cb0 100644 --- a/lib/banzai/pipeline/wiki_pipeline.rb +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -2,8 +2,10 @@ module Banzai module Pipeline class WikiPipeline < FullPipeline def self.filters - @filters ||= super.insert_after(Filter::TableOfContentsFilter, - Filter::GollumTagsFilter) + @filters ||= begin + super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) + .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) + end end end end diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb index 28a2391dbf8..e5e9fab3f5c 100644 --- a/lib/gitlab/badge/build.rb +++ b/lib/gitlab/badge/build.rb @@ -4,14 +4,15 @@ module Gitlab # Build badge # class Build + include Gitlab::Application.routes.url_helpers + include ActionView::Helpers::AssetTagHelper + include ActionView::Helpers::UrlHelper + def initialize(project, ref) + @project, @ref = project, ref @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) end - def to_s - @image[:name].sub(/\.svg$/, '') - end - def type 'image/svg+xml' end @@ -19,6 +20,27 @@ module Gitlab def data File.read(@image[:path]) end + + def to_s + @image[:name].sub(/\.svg$/, '') + end + + def to_html + link_to(image_tag(image_url, alt: 'build status'), link_url) + end + + def to_markdown + "[![build status](#{image_url})](#{link_url})" + end + + def image_url + build_namespace_project_badges_url(@project.namespace, + @project, @ref, format: :svg) + end + + def link_url + namespace_project_commits_url(@project.namespace, @project, id: @ref) + end end end end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index da4435c7308..f2b649e50a2 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -33,7 +33,10 @@ module Gitlab def allowed? if ldap_user - return true unless ldap_config.active_directory + unless ldap_config.active_directory + user.activate if user.ldap_blocked? + return true + end # Block user in GitLab if he/she was blocked in AD if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 88a265c6af2..4a3f47b5a95 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -70,6 +70,32 @@ module Gitlab value.to_s.gsub('=', '\\=') end + # Measures the execution time of a block. + # + # Example: + # + # Gitlab::Metrics.measure(:find_by_username_timings) do + # User.find_by_username(some_username) + # end + # + # series - The name of the series to store the data in. + # values - A Hash containing extra values to add to the metric. + # tags - A Hash containing extra tags to add to the metric. + # + # Returns the value yielded by the supplied block. + def self.measure(series, values = {}, tags = {}) + return yield unless Transaction.current + + start = Time.now.to_f + retval = yield + duration = (Time.now.to_f - start) * 1000.0 + values = values.merge(duration: duration) + + Transaction.current.add_metric(series, values, tags) + + retval + 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/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb new file mode 100644 index 00000000000..32c1c9ec5bb --- /dev/null +++ b/lib/gitlab/saml/auth_hash.rb @@ -0,0 +1,19 @@ +module Gitlab + module Saml + class AuthHash < Gitlab::OAuth::AuthHash + + def groups + get_raw(Gitlab::Saml::Config.groups) + end + + private + + def get_raw(key) + # Needs to call `all` because of https://git.io/vVo4u + # otherwise just the first value is returned + auth_hash.extra[:raw_info].all[key] + end + + end + end +end diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb new file mode 100644 index 00000000000..0f40c00f547 --- /dev/null +++ b/lib/gitlab/saml/config.rb @@ -0,0 +1,21 @@ +module Gitlab + module Saml + class Config + + class << self + def options + Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' } + end + + def groups + options[:groups_attribute] + end + + def external_groups + options[:external_groups] + end + end + + end + end +end diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index b1e30110ef5..c1072452abe 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -18,7 +18,7 @@ module Gitlab @user ||= find_or_create_ldap_user end - if auto_link_saml_enabled? + if auto_link_saml_user? @user ||= find_by_email end @@ -26,6 +26,16 @@ module Gitlab @user ||= build_new_user end + if external_users_enabled? + # Check if there is overlap between the user's groups and the external groups + # setting then set user as external or internal. + if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? + @user.external = false + else + @user.external = true + end + end + @user end @@ -37,11 +47,23 @@ module Gitlab end end + def changed? + gl_user.changed? || gl_user.identities.any?(&:changed?) + end + protected - def auto_link_saml_enabled? + def auto_link_saml_user? Gitlab.config.omniauth.auto_link_saml_user end + + def external_users_enabled? + !Gitlab::Saml::Config.external_groups.nil? + end + + def auth_hash=(auth_hash) + @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash) + end end end end diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb new file mode 100644 index 00000000000..2ba0d489197 --- /dev/null +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Admin::ProjectsController do + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + + before do + sign_in(create(:admin)) + end + + describe 'GET /projects' do + render_views + + it 'retrieves the project for the given visibility level' do + get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC] + expect(response.body).to match(project.name) + end + + it 'does not retrieve the project' do + get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] + expect(response.body).to_not match(project.name) + end + end +end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 98ae424ed7c..8ad73472117 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -93,6 +93,20 @@ describe Projects::BranchesController do end end + describe "POST destroy with HTML format" do + render_views + + it 'returns 303' do + post :destroy, + format: :html, + id: 'foo/bar/baz', + namespace_id: project.namespace.to_param, + project_id: project.to_param + + expect(response.status).to eq(303) + end + end + describe "POST destroy" do render_views diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb new file mode 100644 index 00000000000..d47e4ab9a4f --- /dev/null +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -0,0 +1,49 @@ +require('spec_helper') + +describe Projects::ProjectMembersController do + let(:project) { create(:project) } + let(:another_project) { create(:project, :private) } + let(:user) { create(:user) } + let(:member) { create(:user) } + + before do + project.team << [user, :master] + another_project.team << [member, :guest] + sign_in(user) + end + + describe '#apply_import' do + shared_context 'import applied' do + before do + post(:apply_import, namespace_id: project.namespace.to_param, + project_id: project.to_param, + source_project_id: another_project.id) + end + end + + context 'when user can access source project members' do + before { another_project.team << [user, :guest] } + include_context 'import applied' + + it 'imports source project members' do + expect(project.team_members).to include member + expect(response).to set_flash.to 'Successfully imported' + expect(response).to redirect_to( + namespace_project_project_members_path(project.namespace, project) + ) + end + end + + context 'when user is not member of a source project' do + include_context 'import applied' + + it 'does not import team members' do + expect(project.team_members).to_not include member + end + + it 'responds with not found' do + expect(response.status).to eq 404 + end + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 1893e946f5c..069cd917e5a 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -83,6 +83,28 @@ describe ProjectsController do end end + describe "#update" do + render_views + + let(:admin) { create(:admin) } + + it "sets the repository to the right path after a rename" do + new_path = 'renamed_path' + project_params = { path: new_path } + controller.instance_variable_set(:@project, project) + sign_in(admin) + + put :update, + namespace_id: project.namespace.to_param, + id: project.id, + project: project_params + + expect(project.repository.path).to include(new_path) + expect(assigns(:repository).path).to eq(project.repository.path) + expect(response.status).to eq(200) + end + end + describe "#destroy" do let(:admin) { create(:admin) } diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb new file mode 100644 index 00000000000..83cc8ec6d26 --- /dev/null +++ b/spec/controllers/sessions_controller_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe SessionsController do + describe '#create' do + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + end + + context 'when using standard authentications' do + context 'invalid password' do + it 'does not authenticate user' do + post(:create, user: { login: 'invalid', password: 'invalid' }) + + expect(response) + .to set_flash.now[:alert].to /Invalid login or password/ + end + end + + context 'when using valid password' do + let(:user) { create(:user) } + + it 'authenticates user correctly' do + post(:create, user: { login: user.username, password: user.password }) + + expect(response).to set_flash.to /Signed in successfully/ + expect(subject.current_user). to eq user + end + end + end + + context 'when using two-factor authentication' do + let(:user) { create(:user, :two_factor) } + + def authenticate_2fa(user_params) + post(:create, { user: user_params }, { otp_user_id: user.id }) + end + + ## + # See #14900 issue + # + context 'when authenticating with login and OTP of another user' do + context 'when another user has 2FA enabled' do + let(:another_user) { create(:user, :two_factor) } + + context 'when OTP is valid for another user' do + it 'does not authenticate' do + authenticate_2fa(login: another_user.username, + otp_attempt: another_user.current_otp) + + expect(subject.current_user).to_not eq another_user + end + end + + context 'when OTP is invalid for another user' do + it 'does not authenticate' do + authenticate_2fa(login: another_user.username, + otp_attempt: 'invalid') + + expect(subject.current_user).to_not eq another_user + end + end + + context 'when authenticating with OTP' do + context 'when OTP is valid' do + it 'authenticates correctly' do + authenticate_2fa(otp_attempt: user.current_otp) + + expect(subject.current_user).to eq user + end + end + + context 'when OTP is invalid' do + before { authenticate_2fa(otp_attempt: 'invalid') } + + it 'does not authenticate' do + expect(subject.current_user).to_not eq user + end + + it 'warns about invalid OTP code' do + expect(response).to set_flash.now[:alert] + .to /Invalid two-factor code/ + end + end + end + + context 'when another user does not have 2FA enabled' do + let(:another_user) { create(:user) } + + it 'does not leak that 2FA is disabled for another user' do + authenticate_2fa(login: another_user.username, + otp_attempt: 'invalid') + + expect(response).to set_flash.now[:alert] + .to /Invalid two-factor code/ + end + end + end + end + end + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb new file mode 100644 index 00000000000..90822a8c123 --- /dev/null +++ b/spec/features/issues/filter_issues_spec.rb @@ -0,0 +1,119 @@ +require 'rails_helper' + +describe 'Filter issues', feature: true do + + let!(:project) { create(:project) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'Filter issues for assignee from issues#index' do + + before do + visit namespace_project_issues_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + sleep 2 + end + + context 'assignee', js: true do + it 'should update to current user' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'should not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + + it 'should not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + end + end + + describe 'Filter issues for milestone from issues#index' do + + before do + visit namespace_project_issues_path(project.namespace, project) + + find('.js-milestone-select').click + + find('.milestone-filter .dropdown-content a', text: milestone.title).click + + sleep 2 + end + + context 'milestone', js: true do + it 'should update to current milestone' do + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + it 'should not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + + it 'should not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + end + end + + describe 'Filter issues for assignee and label from issues#index' do + + before do + visit namespace_project_issues_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + sleep 2 + + find('.js-label-select').click + + find('.dropdown-menu-labels .dropdown-content a', text: label.title).click + + sleep 2 + end + + context 'assignee and label', js: true do + it 'should update to current assignee and label' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it 'should not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + + it 'should not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index db46657c36a..79000666ccc 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -22,7 +22,7 @@ describe 'Issues', feature: true do before do visit edit_namespace_project_issue_path(project.namespace, project, issue) - click_link "Edit" + click_button "Go full screen" end it 'should open new issue popup' do diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 12fd8d37210..3d0d0e59fd7 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do end def doc(html = @html) - Nokogiri::HTML::DocumentFragment.parse(html) + @doc ||= Nokogiri::HTML::DocumentFragment.parse(html) end # Shared behavior that all pipelines should exhibit @@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do file = Gollum::File.new(@project_wiki.wiki) expect(file).to receive(:path).and_return('images/example.jpg') expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) + allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index d9a8058efd9..70d0864783d 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -152,7 +152,7 @@ describe 'Comments', feature: true do it 'has .new_note css class' do page.within('.js-temp-notes-holder') do - expect(subject).to have_css('.new_note') + expect(subject).to have_css('.new-note') end end end @@ -225,6 +225,6 @@ describe 'Comments', feature: true do end def click_diff_line(data = line_code) - page.find(%Q{button[data-line-code="#{data}"]}, visible: false).click + execute_script("$('button[data-line-code=\"#{data}\"]').click()") end end diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb new file mode 100644 index 00000000000..13c9b95b316 --- /dev/null +++ b/spec/features/projects/badges/list_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +feature 'list of badges' do + include Select2Helper + + background do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as(user) + visit edit_namespace_project_path(project.namespace, project) + end + + scenario 'user displays list of badges' do + click_link 'Badges' + + expect(page).to have_content 'build status' + expect(page).to have_content 'Markdown' + expect(page).to have_content 'HTML' + expect(page).to have_css('.highlight', count: 2) + expect(page).to have_xpath("//img[@alt='build status']") + + page.within('.highlight', match: :first) do + expect(page).to have_content 'badges/master/build.svg' + end + end + + scenario 'user changes current ref on badges list page', js: true do + click_link 'Badges' + select2('improve/awesome', from: '#ref') + + expect(page).to have_content 'badges/improve/awesome/build.svg' + end +end diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml index 1701652c61e..cb906a7feaa 100644 --- a/spec/javascripts/fixtures/zen_mode.html.haml +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -1,4 +1,4 @@ -.zennable +.md-area .zen-backdrop %textarea#note_note.js-gfm-input.markdown-area %a.js-zen-enter(tabindex="-1" href="#") diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 86ba9dd8e96..ea27f36e9b5 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -29,8 +29,8 @@ describe 'reopen/close issue', -> spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://gitlab.com/issues/6/close') - req.success saved: true - + req.success id: 34 + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') expect($btnReopen).toBeHidden() @@ -94,7 +94,7 @@ describe 'reopen/close issue', -> spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://gitlab.com/issues/6/reopen') - req.success saved: true + req.success id: 34 $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index 5e23c5c319a..fe2ce092e6b 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do end context 'linking internal resources' do - it "the created link's text will be equal to the resource's text" do + it "the created link's text includes the resource's text and wiki base path" do tag = '[[wiki-slug]]' doc = filter("See #{tag}", project_wiki: project_wiki) + expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug') expect(doc.at_css('a').text).to eq 'wiki-slug' - expect(doc.at_css('a')['href']).to eq 'wiki-slug' + expect(doc.at_css('a')['href']).to eq expected_path end it "the created link's text will be link-text" do tag = '[[link-text|wiki-slug]]' doc = filter("See #{tag}", project_wiki: project_wiki) + expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug') expect(doc.at_css('a').text).to eq 'link-text' - expect(doc.at_css('a')['href']).to eq 'wiki-slug' + expect(doc.at_css('a')['href']).to eq expected_path end end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 5a0d3d577a8..266ebef33d6 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do result = reference_pipeline_result("Fixed #{reference}") expect(result[:references][:issue]).to eq [issue] end + + it 'does not process links containing issue numbers followed by text' do + href = "#{reference}st" + doc = reference_filter("<a href='#{href}'></a>") + link = doc.css('a').first.attr('href') + + expect(link).to eq(href) + end end context 'cross-project reference' do diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 3e25406e498..7aa1b4a3bf6 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do Foo MD - result = described_class.call(markdown, project: spy, project_wiki: double) + result = described_class.call(markdown, project: spy, project_wiki: spy) aggregate_failures do expect(result[:output].text).not_to include '[[' @@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do Foo MD - output = described_class.to_html(markdown, project: spy, project_wiki: double) + output = described_class.to_html(markdown, project: spy, project_wiki: spy) expect(output).to include('[[<em>toc</em>]]') end @@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do Foo MD - output = described_class.to_html(markdown, project: spy, project_wiki: double) + output = described_class.to_html(markdown, project: spy, project_wiki: spy) aggregate_failures do expect(output).not_to include('<ul>') diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index b78c2b6224f..329792bb685 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -3,13 +3,44 @@ require 'spec_helper' describe Gitlab::Badge::Build do let(:project) { create(:project) } let(:sha) { project.commit.sha } - let(:badge) { described_class.new(project, 'master') } + let(:branch) { 'master' } + let(:badge) { described_class.new(project, branch) } describe '#type' do subject { badge.type } it { is_expected.to eq 'image/svg+xml' } end + describe '#to_html' do + let(:html) { Nokogiri::HTML.parse(badge.to_html) } + let(:a_href) { html.at('a') } + + it 'points to link' do + expect(a_href[:href]).to eq badge.link_url + end + + it 'contains clickable image' do + expect(a_href.children.first.name).to eq 'img' + end + end + + describe '#to_markdown' do + subject { badge.to_markdown } + + it { is_expected.to include badge.image_url } + it { is_expected.to include badge.link_url } + end + + describe '#image_url' do + subject { badge.image_url } + it { is_expected.to include "badges/#{branch}/build.svg" } + end + + describe '#link_url' do + subject { badge.link_url } + it { is_expected.to include "commits/#{branch}" } + end + context 'build exists' do let(:ci_commit) { create(:ci_commit, project: project, sha: sha) } let!(:build) { create(:ci_build, commit: ci_commit) } diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 32a19bf344b..f5b66b8156f 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do access.allowed? expect(user).to be_blocked expect(user).to be_ldap_blocked @@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do end it { is_expected.to be_truthy } + + context 'when user cannot be found' do + before do + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil) + end + + it { is_expected.to be_falsey } + + it 'blocks user in GitLab' do + access.allowed? + expect(user).to be_blocked + expect(user).to be_ldap_blocked + end + end + + context 'when user was previously ldap_blocked' do + before do + user.ldap_block + end + + it 'unblocks the user if it exists' do + access.allowed? + expect(user).not_to be_blocked + end + end end end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 0ec8a6dc5cb..8f63a5f2043 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Metrics do end end - describe '#submit_metrics' do + describe '.submit_metrics' do it 'prepares and writes the metrics to InfluxDB' do connection = double(:connection) pool = double(:pool) @@ -26,7 +26,7 @@ describe Gitlab::Metrics do end end - describe '#prepare_metrics' do + describe '.prepare_metrics' do it 'returns a Hash with the keys as Symbols' do metrics = described_class. prepare_metrics([{ 'values' => {}, 'tags' => {} }]) @@ -51,7 +51,7 @@ describe Gitlab::Metrics do end end - describe '#escape_value' do + describe '.escape_value' do it 'escapes an equals sign' do expect(described_class.escape_value('foo=')).to eq('foo\\=') end @@ -60,4 +60,45 @@ describe Gitlab::Metrics do expect(described_class.escape_value(10)).to eq('10') end end + + describe '.measure' do + context 'without a transaction' do + it 'returns the return value of the block' do + val = Gitlab::Metrics.measure(:foo) { 10 } + + expect(val).to eq(10) + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + before do + allow(Gitlab::Metrics::Transaction).to receive(:current). + and_return(transaction) + end + + it 'adds a metric to the current transaction' do + expect(transaction).to receive(:add_metric). + with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' }) + + Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 } + end + + it 'supports adding of custom values' do + values = { duration: a_kind_of(Numeric), number: 10 } + + expect(transaction).to receive(:add_metric). + with(:foo, values, { tag: 'value' }) + + Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 } + end + + it 'returns the return value of the block' do + val = Gitlab::Metrics.measure(:foo) { 10 } + + expect(val).to eq(10) + end + end + end end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index de7cd99d49d..c2a51d9249c 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do let(:gl_user) { saml_user.gl_user } let(:uid) { 'my-uid' } let(:provider) { 'saml' } - let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } + let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) } let(:info_hash) do { name: 'John', @@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do allow(Gitlab::LDAP::Config).to receive_messages(messages) end + def stub_basic_saml_config + allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } }) + end + + def stub_saml_group_config(groups) + allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) + end + + before { stub_basic_saml_config } + describe 'account exists on server' do before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } context 'and should bind with SAML' do - let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } it 'adds the SAML identity to the existing user' do saml_user.save expect(gl_user).to be_valid @@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do expect(identity.provider).to eql 'saml' end end + + context 'external groups' do + context 'are defined' do + it 'marks the user as external' do + stub_saml_group_config(%w(Freelancers)) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + before { stub_saml_group_config(%w(Interns)) } + context 'are defined but the user does not belong there' do + it 'does not mark the user as external' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + + context 'user was external, now should not be' do + it 'should make user internal' do + existing_user.update_attribute('external', true) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + end end describe 'no account exists on server' do @@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do end end + context 'external groups' do + context 'are defined' do + it 'marks the user as external' do + stub_saml_group_config(%w(Freelancers)) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + context 'are defined but the user does not belong there' do + it 'does not mark the user as external' do + stub_saml_group_config(%w(Interns)) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + end + context 'with auto_link_ldap_user disabled (default)' do before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } include_examples 'to verify compliance with allow_single_sign_on' @@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do context 'with auto_link_ldap_user enabled' do before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } - context 'and no LDAP provider defined' do - before { stub_ldap_config(providers: []) } - - include_examples 'to verify compliance with allow_single_sign_on' - end - context 'and at least one LDAP provider is defined' do before { stub_ldap_config(providers: %w(ldapmain)) } @@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do before do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } - allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context 'and no account for the LDAP user' do - it 'creates a user with dual LDAP and SAML identities' do saml_user.save expect(gl_user).to be_valid expect(gl_user.username).to eql uid - expect(gl_user.email).to eql 'johndoe@example.com' + expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to eql 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, @@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do end context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } - it "adds the omniauth identity to the LDAP account" do + let!(:existing_user) { create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + it 'adds the omniauth identity to the LDAP account' do saml_user.save expect(gl_user).to be_valid expect(gl_user.username).to eql 'john' - expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to eql 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, @@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do end end end - - context 'and no corresponding LDAP person' do - before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } - - include_examples 'to verify compliance with allow_single_sign_on' - end end end end describe 'blocking' do - before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) } + before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } context 'signup with SAML only' do context 'dont block on create' do @@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do end end - context 'signup with linked omniauth and LDAP account' do - before do - stub_omniauth_config(auto_link_ldap_user: true) - allow(ldap_user).to receive(:uid) { uid } - allow(ldap_user).to receive(:username) { uid } - allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(saml_user).to receive(:ldap_person).and_return(ldap_user) - end - - context "and no account for the LDAP user" do - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).to be_blocked - end - end - end - - context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } - - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - end - end - - context 'sign-in' do before do saml_user.save @@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do expect(gl_user).not_to be_blocked end end - - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end end end end diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 56a6dbf96f9..5a85cb501dd 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -141,10 +141,12 @@ shared_examples 'a new user email' do end shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text '<script type="application/ld+json">' } it { is_expected.to have_body_text /ViewAction/ } end shared_examples 'it should not have Gmail Actions links' do + it { is_expected.to_not have_body_text '<script type="application/ld+json">' } it { is_expected.to_not have_body_text /ViewAction/ } end diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb index 905379a64e3..2ccbff553f0 100644 --- a/spec/models/project_services/builds_email_service_spec.rb +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -6,18 +6,38 @@ describe BuildsEmailService do let(:service) { BuildsEmailService.new } describe :execute do - it "sends email" do + it 'sends email' do service.recipients = 'test@gitlab.com' data[:build_status] = 'failed' expect(BuildEmailWorker).to receive(:perform_async) service.execute(data) end - it "does not sends email with failed build and allowed_failure on" do + it 'does not send email with succeeded build and notify_only_broken_builds on' do + expect(service).to receive(:notify_only_broken_builds).and_return(true) + data[:build_status] = 'success' + expect(BuildEmailWorker).not_to receive(:perform_async) + service.execute(data) + end + + it 'does not send email with failed build and build_allow_failure is true' do data[:build_status] = 'failed' data[:build_allow_failure] = true expect(BuildEmailWorker).not_to receive(:perform_async) service.execute(data) end + + it 'does not send email with unknown build status' do + data[:build_status] = 'foo' + expect(BuildEmailWorker).not_to receive(:perform_async) + service.execute(data) + end + + it 'does not send email when recipients list is empty' do + service.recipients = ' ,, ' + data[:build_status] = 'failed' + expect(BuildEmailWorker).not_to receive(:perform_async) + service.execute(data) + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c5d5a1c2492..4e49c413f23 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -612,6 +612,20 @@ describe Repository, models: true do end end + describe '#before_import' do + it 'flushes the emptiness cachess' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.before_import + end + + it 'flushes the exists cache' do + expect(repository).to receive(:expire_exists_cache) + + repository.before_import + end + end + describe '#after_import' do it 'flushes the emptiness cachess' do expect(repository).to receive(:expire_emptiness_caches) @@ -656,6 +670,19 @@ describe Repository, models: true do repository.after_create end + + it 'flushes the root ref cache' do + expect(repository).to receive(:expire_root_ref_cache) + + repository.after_create + end + + it 'flushes the emptiness caches' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.after_create + end + end describe "#main_language" do diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index db0f6e3c0f5..d97bf6d38ff 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -4,6 +4,7 @@ describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } + let!(:closed_milestone) { create(:closed_milestone, project: project) } let!(:milestone) { create(:milestone, project: project) } before { project.team << [user, :developer] } @@ -20,6 +21,24 @@ describe API::API, api: true do get api("/projects/#{project.id}/milestones") expect(response.status).to eq(401) end + + it 'returns an array of active milestones' do + get api("/projects/#{project.id}/milestones?state=active", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(milestone.id) + end + + it 'returns an array of closed milestones' do + get api("/projects/#{project.id}/milestones?state=closed", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_milestone.id) + end end describe 'GET /projects/:id/milestones/:milestone_id' do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 8490a729e51..b40a5c1c818 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -159,18 +159,44 @@ describe GitPushService, services: true do end describe "Updates main language" do - context "before push" do it { expect(project.main_language).to eq(nil) } end context "after push" do - before do - @service = execute_service(project, user, @oldrev, @newrev, @ref) + def execute + execute_service(project, user, @oldrev, @newrev, ref) end - it { expect(@service.update_main_language).to eq(true) } - it { expect(project.main_language).to eq("Ruby") } + context "to master" do + let(:ref) { @ref } + + context 'when main_language is nil' do + it 'obtains the language from the repository' do + expect(project.repository).to receive(:main_language) + execute + end + + it 'sets the project main language' do + execute + expect(project.main_language).to eq("Ruby") + end + end + + context 'when main_language is already set' do + it 'does not check the repository' do + execute # do an initial run to simulate lang being preset + expect(project.repository).not_to receive(:main_language) + execute + end + end + end + + context "to other branch" do + let(:ref) { 'refs/heads/feature/branch' } + + it { expect(project.main_language).to eq(nil) } + end end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 04f474c736c..32bf3acf483 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -72,6 +72,23 @@ describe Projects::ImportService, services: true do expect(result[:status]).to eq :success end + it 'flushes various caches' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository). + with(project.path_with_namespace, project.import_url). + and_return(true) + + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute). + and_return(true) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). + and_call_original + + expect_any_instance_of(Repository).to receive(:expire_exists_cache). + and_call_original + + subject.execute + end + it 'fails if importer fails' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 1d52489e804..43cb6ef43f2 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -13,7 +13,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - link = actual.at_css('a:contains("Relative Link")') + link = actual.at_css('a:contains("Relative Link")') image = actual.at_css('img[alt="Relative Image"]') expect(link['href']).to end_with('master/doc/README.md') @@ -72,14 +72,15 @@ module MarkdownMatchers have_css("img[src$='#{src}']") end + prefix = '/namespace1/gitlabhq/wikis' set_default_markdown_messages match do |actual| - expect(actual).to have_link('linked-resource', href: 'linked-resource') - expect(actual).to have_link('link-text', href: 'linked-resource') + expect(actual).to have_link('linked-resource', href: "#{prefix}/linked-resource") + expect(actual).to have_link('link-text', href: "#{prefix}/linked-resource") expect(actual).to have_link('http://example.com', href: 'http://example.com') expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf') - expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg') + expect(actual).to have_image("#{prefix}/images/example.jpg") expect(actual).to have_image('http://example.com/images/example.jpg') end end |