diff options
320 files changed, 5243 insertions, 1380 deletions
diff --git a/CHANGELOG b/CHANGELOG index 4221fb9df55..a15bbfbc49e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.4.0 (unreleased) + - Add pagination headers to already paginated API resources + - Properly generate diff of orphan commits, like the first commit in a repository + - Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages - Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse) - Improved performance of finding issues for an entire group (Yorick Peterse) - Added custom application performance measuring system powered by InfluxDB (Yorick Peterse) @@ -13,8 +16,11 @@ v 8.4.0 (unreleased) - Fix missing date of month in network graph when commits span a month (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) + - Remove gray background from layout in UI + - Fix signup for OAuth providers that don't provide a name - Implement new UI for group page - Implement search inside emoji picker + - Let the CI runner know about builds that this build depends on - Add API support for looking up a user by username (Stan Hu) - Add project permissions to all project API endpoints (Stan Hu) - Link to milestone in "Milestone changed" system note @@ -42,10 +48,20 @@ v 8.4.0 (unreleased) - Ajax filter by message for commits page - API: Add support for deleting a tag via the API (Robert Schilling) - Allow subsequent validations in CI Linter + - Show referenced MRs & Issues only when the current viewer can access them - Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee) + - Add API support for managing project's builds + - Add API support for managing project's build triggers + - Add API support for managing project's build variables + - Allow broadcast messages to be edited + - Autosize Markdown textareas + - Import GitHub wiki into GitLab + - Add reporters ability to download and browse build artifacts (Andrew Johnson) + - Autofill referring url in message box when reporting user abuse. (Josh Frye) v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) + - Add build artifacts browser v 8.3.3 - Preserve CE behavior with JIRA integration by only calling API if URL is set @@ -61,6 +77,7 @@ v 8.3.3 - Use WOFF versions of SourceSansPro fonts - Fix regression when builds were not generated for tags created through web/api interface - Fix: maintain milestone filter between Open and Closed tabs (Greg Smethells) + - Fix missing artifacts and build traces for build created before 8.3 v 8.3.2 - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu) @@ -78,6 +95,7 @@ v 8.3.0 - Add open_issues_count to project API (Stan Hu) - Expand character set of usernames created by Omniauth (Corey Hinshaw) - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) + - Add unsubscribe link in the email footer (Zeger-Jan van de Weg) - Provide better diagnostic message upon project creation errors (Stan Hu) - Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu) - Remove api credentials from link to build_page @@ -2,6 +2,6 @@ # https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in # lib/support/init.d, which call scripts in bin/ . # -web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default +web: RAILS_ENV=development bin/web start_foreground +worker: RAILS_ENV=development bin/background_jobs start_foreground # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..c90d078406c --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..b87e22c41b5 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..0f46f3e833a --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..8007df6df32 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..b715f274082 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..d8f9d29d4aa --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..a00852641f8 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-It.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..d8b610ad76e --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..e0eebac8273 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..0dd3464c74b --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..2526d2e1b60 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..606935af089 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 63803747413..3b6b453ac51 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,7 +1,7 @@ class @Activities constructor: -> Pager.init 20, true - $(".event-filter .btn").bind "click", (event) => + $(".event-filter a").bind "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() @@ -12,7 +12,7 @@ class @Activities toggleFilter: (sender) -> - sender.toggleClass "active" + sender.closest('li').toggleClass "active" event_filters = $.cookie("event_filter") filter = sender.attr("id").split("_")[0] if event_filters diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee index bcb2e6df7c0..eb951f71711 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -10,19 +10,19 @@ class @Admin $('body').on 'click', '.js-toggle-colors-link', (e) -> e.preventDefault() - $('.js-toggle-colors-link').hide() - $('.js-toggle-colors-container').show() + $('.js-toggle-colors-container').toggle() $('input#broadcast_message_color').on 'input', -> - previewColor = $('input#broadcast_message_color').val() + previewColor = $(@).val() $('div.broadcast-message-preview').css('background-color', previewColor) $('input#broadcast_message_font').on 'input', -> - previewColor = $('input#broadcast_message_font').val() + previewColor = $(@).val() $('div.broadcast-message-preview').css('color', previewColor) $('textarea#broadcast_message_message').on 'input', -> - previewMessage = $('textarea#broadcast_message_message').val() + previewMessage = $(@).val() + previewMessage = "Your message here" if previewMessage.trim() == '' $('div.broadcast-message-preview span').text(previewMessage) $('.log-tabs a').click (e) -> diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee new file mode 100644 index 00000000000..b32072e61ee --- /dev/null +++ b/app/assets/javascripts/behaviors/autosize.js.coffee @@ -0,0 +1,4 @@ +#= require autosize + +$ -> + autosize($('.js-autosize')) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 239f3d6afae..6af5a48a0bb 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -64,14 +64,15 @@ class @MergeRequest _this = @ $('a.btn-close, a.btn-reopen').on 'click', (e) -> $this = $(this) - if $this.data('submitted') - return - e.preventDefault() - e.stopImmediatePropagation() shouldSubmit = $this.hasClass('btn-comment') - console.log("shouldSubmit") + if shouldSubmit && $this.data('submitted') + return if shouldSubmit - _this.submitNoteForm($this.closest('form'),$this) + if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen') + e.preventDefault() + e.stopImmediatePropagation() + _this.submitNoteForm($this.closest('form'),$this) + submitNoteForm: (form, $button) => noteText = form.find("textarea.js-note-text").val() diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 9e2dc1250c9..b10e1db7f3f 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -5,7 +5,7 @@ # # ### Example Markup # -# <ul class="nav nav-tabs merge-request-tabs"> +# <ul class="nav-links merge-request-tabs"> # <li class="notes-tab active"> # <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> # Discussion diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 8ba00ecbbab..2bfc5cb2d9c 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,4 +1,5 @@ #= require autosave +#= require autosize #= require dropzone #= require dropzone_input #= require gfm_auto_complete @@ -246,6 +247,7 @@ class @Notes else previewButton.removeClass("turn-on").addClass "turn-off" + autosize(textarea) new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -318,6 +320,7 @@ class @Notes form.show() textarea = form.find("textarea") textarea.focus() + autosize(textarea) # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # The textarea has the correct value, Chrome just won't show it unless we @@ -353,7 +356,7 @@ class @Notes $('.note[id="' + note_id + '"]').each -> note = $(this) notes = note.closest(".notes") - count = notes.closest(".notes_holder").find(".discussion-notes-count") + count = notes.closest(".issuable-details").find(".notes-tab .badge") # check if this is the last note for this line if notes.find(".note").length is 1 @@ -363,9 +366,10 @@ class @Notes # for diff lines notes.closest("tr").remove() - else - # update notes count - count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}" + + # update notes count + oldNum = parseInt(count.text()) + count.text(oldNum - 1) note.remove() @@ -521,9 +525,13 @@ class @Notes if textarea.val().trim().length > 0 form.find('.js-note-target-reopen').text('Comment & reopen') form.find('.js-note-target-close').text('Comment & close') + form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen') + form.find('.js-note-target-close').addClass('btn-comment-and-close') else form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-close').text('Close') + form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen') + form.find('.js-note-target-close').removeClass('btn-comment-and-close') initTaskList: -> @enableTaskList() diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 81cfc37b956..19420f42468 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -1,17 +1,18 @@ +#= require latinise + class @Wikis constructor: -> - $('.build-new-wiki').bind "click", (e) -> - $('[data-error~=slug]').addClass("hidden") - $('p.hint').show() + $('.build-new-wiki').bind 'click', (e) => + $('[data-error~=slug]').addClass('hidden') field = $('#new_wiki_path') - valid_slug_pattern = /^[\w\/-]+$/ + slug = @slugify(field.val()) - slug = field.val() - if slug.match valid_slug_pattern + if (slug.length > 0) path = field.attr('data-wikis-path') - if(slug.length > 0) - location.href = path + "/" + slug - else - e.preventDefault() - $('p.hint').hide() - $('[data-error~=slug]').removeClass("hidden") + location.href = path + '/' + slug + + dasherize: (value) -> + value.replace(/[_\s]+/g, '-') + + slugify: (value) => + @dasherize(value.trim().toLowerCase().latinise()) diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 48a4971c8fc..fa7641b1676 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -24,6 +24,7 @@ @import "framework/lists.scss"; @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; +@import "framework/nav.scss"; @import "framework/pagination.scss"; @import "framework/panels.scss"; @import "framework/selects.scss"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index fa0e70847f3..d0f5d33bf4d 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -18,9 +18,9 @@ line-height: 36px; } -.content-block, .gray-content-block { - margin: -$gl-padding; + margin-top: 0; + margin-bottom: -$gl-padding; background-color: $background-color; padding: $gl-padding; margin-bottom: 0px; @@ -86,10 +86,7 @@ .cover-block { text-align: center; background: $background-color; - margin: -$gl-padding; - margin-bottom: 0; - padding: 44px $gl-padding; - border-bottom: 1px solid $border-color; + padding-top: 44px; position: relative; .avatar-holder { @@ -136,3 +133,19 @@ .block-connector { margin-top: -1px; } + +.nav-block { + .controls { + float: right; + margin-top: 11px; + } +} + +.content-block { + padding: $gl-padding 0; + border-bottom: 1px solid $border-color; + + &.oneline-block { + line-height: 42px; + } +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 97a94638847..c99292c3f83 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,12 +1,8 @@ @mixin btn-default { @include border-radius(3px); - border-width: 1px; - border-style: solid; - font-size: 15px; + font-size: $gl-font-size; font-weight: 500; - line-height: 18px; - padding: 11px $gl-padding; - letter-spacing: .4px; + padding: $gl-vert-padding $gl-padding; &:focus, &:active { @@ -17,8 +13,6 @@ @mixin btn-middle { @include btn-default; - @include border-radius(3px); - padding: 11px 24px; } @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { @@ -74,16 +68,15 @@ @include btn-default; @include btn-white; + &.btn-small, &.btn-sm { - padding: 5px 10px; - } - - &.btn-nr { - padding: 7px 10px; + padding: 4px 10px; + font-size: 13px; + line-height: 18px; } &.btn-xs { - padding: 1px 5px; + padding: 2px 5px; } &.btn-success, @@ -131,6 +124,12 @@ &:last-child { margin-right: 0px; } + &.btn-xs { + margin-right: 3px; + } + } + &.disabled { + pointer-events: auto !important; } } @@ -153,33 +152,42 @@ } } -.btn-group-next { +.btn-clipboard { + border: none; + padding: 0 5px; +} + +.input-group-btn { .btn { - padding: 9px 0px; - font-size: 15px; - color: #7f8fa4; - border-color: #e7e9ed; - width: 140px; - - .badge { - font-weight: normal; - background-color: #eee; - color: #78a; + @include btn-gray; + @include btn-middle; + + &:hover { + outline: none; } - &.active { - border-color: $gl-info; - background: $gl-info; - color: #fff; + &:focus { + outline: none; + } + + &:active { + outline: none; + } - .badge { - color: $gl-info; - background-color: white; - } + &.btn-clipboard { + padding-left: 15px; + padding-right: 15px; } } -} -.btn-clipboard { - border: none; + .active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + border: 1px solid #c6cacf !important; + background-color: #e4e7ed !important; + } + + .btn-green { + @include btn-green + } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 11730000f85..585a9d83913 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -75,6 +75,8 @@ hr { @include str-truncated; } +.item-title { font-weight: 600; } + /** FLASH message **/ .author_link { color: $gl-link-color; @@ -374,75 +376,6 @@ table { } } -.center-top-menu, .left-top-menu { - @include nav-menu; - text-align: center; - margin-top: 5px; - margin-bottom: $gl-padding; - height: auto; - margin-top: -$gl-padding; - - &.no-bottom { - margin-bottom: 0; - } - - &.no-top { - margin-top: 0; - } - - li a { - display: inline-block; - padding-top: $gl-padding; - padding-bottom: 11px; - margin-bottom: -1px; - } - - &.bottom-border { - border-bottom: 1px solid $border-color; - height: 57px; - } - - &.wide { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - } -} - -.left-top-menu { - text-align: left; - border-bottom: 1px solid #EEE; -} - -.center-middle-menu { - @include nav-menu; - padding: 0; - text-align: center; - margin: -$gl-padding; - margin-top: 0; - margin-bottom: 0; - height: 58px; - border-bottom: 1px solid $border-color; - - li { - &:after { - content: "|"; - color: $border-gray-light; - } - - &:last-child { - &:after { - content: none; - } - } - - > a { - display: inline-block; - text-transform: uppercase; - font-size: 13px; - } - } -} - .dropzone .dz-preview .dz-progress { border-color: $border-color !important; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index cbfd4bc29b6..6ee104ee31a 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -3,11 +3,8 @@ * */ .file-holder { - margin-left: -$gl-padding; - margin-right: -$gl-padding; border: none; - border-top: 1px solid #E7E9EE; - border-bottom: 1px solid #E7E9EE; + border: 1px solid $border-color; &.readme-holder { border-bottom: 0; diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 82eb50ad4be..1bfd0213995 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -8,10 +8,12 @@ .flash-notice { @extend .alert; @extend .alert-info; + margin: 0; } .flash-alert { @extend .alert; @extend .alert-danger; + margin: 0; } } diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss index 20988f7b430..7a946109e3a 100644 --- a/app/assets/stylesheets/framework/fonts.scss +++ b/app/assets/stylesheets/framework/fonts.scss @@ -3,23 +3,39 @@ font-family: 'Source Sans Pro'; font-style: normal; font-weight: 300; - src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf.woff'); + src: + local('Source Sans Pro Light'), + local('SourceSansPro-Light'), + font-url('SourceSansPro-Light.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Light.ttf.woff') format('woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 400; - src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf.woff'); + src: + local('Source Sans Pro'), + local('SourceSansPro-Regular'), + font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Regular.ttf.woff') format('woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 600; - src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf.woff'); + src: + local('Source Sans Pro Semibold'), + local('SourceSansPro-Semibold'), + font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Semibold.ttf.woff') format('woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 700; - src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf.woff'); + src: + local('Source Sans Pro Bold'), + local('SourceSansPro-Bold'), + font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Bold.ttf.woff') format('woff'); } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 032d343df44..4dab806d50e 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -74,8 +74,10 @@ label { .form-control { @include box-shadow(none); - height: 42px; - padding: 8px $gl-padding; +} + +.form-control-inline { + display: inline; } .wiki-content { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4dbbb56104b..ba5e72c8c5a 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -28,6 +28,7 @@ header { min-height: $header-height; background-color: #fff; border: none; + border-bottom: 1px solid #EEE; .container-fluid { width: 100% !important; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index a1a9990241d..e901c78d02f 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -5,8 +5,6 @@ html { } body { - background-color: #F3F3F3 !important; - &.navless { background-color: white !important; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index bbdb1c038c5..c6bc6fb324d 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -109,10 +109,8 @@ ul.content-list { padding: 0; > li { - padding: $gl-padding; + padding: $gl-padding 0; border-color: $table-border-color; - margin-left: -$gl-padding; - margin-right: -$gl-padding; color: $gl-gray; .avatar { @@ -133,6 +131,7 @@ ul.content-list { .panel > .content-list { li { margin: 0; + padding: $gl-padding; } } @@ -148,7 +147,7 @@ ul.controls { > li { float: left; margin-right: 10px; - + &:last-child { margin-right: 0; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 4a00a197d9a..6732343802a 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -65,13 +65,6 @@ position: relative; } -.md-header { - ul { - float: left; - margin-bottom: 1px; - } -} - .referenced-users { color: #4c4e54; padding-top: 10px; @@ -85,28 +78,12 @@ box-shadow: none; } -.new_note, -.edit_note, -.detail-page-description, -.milestone-description, -.wiki-content, -.merge-request-form { - .nav-tabs { - margin-bottom: 0; - border: none; - - li a, - li.active a { - border: 1px solid #DDD; - } - } -} - .markdown-area { @include border-radius(0); background: #FFF; border: 1px solid #ddd; min-height: 140px; + max-height: 430px; padding: 5px; box-shadow: none; width: 100%; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 41fd890f14f..1d5000fe388 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -118,38 +118,3 @@ font-size: 16px; line-height: 24px; } - -@mixin nav-menu { - padding: 0; - margin: 0; - list-style: none; - height: 56px; - - li { - display: inline-block; - - a { - padding: 14px; - font-size: 15px; - line-height: 28px; - color: #959494; - border-bottom: 2px solid transparent; - - &:hover, &:active, &:focus { - text-decoration: none; - outline: none; - } - } - - &.active a { - color: #616060; - border-bottom: 2px solid #4688f1; - } - - .badge { - font-weight: normal; - background-color: #eee; - color: #78a; - } - } -} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index c00709fb6bb..0997dfc287c 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -9,7 +9,7 @@ padding-right: 5px; } - .nav.nav-tabs > li > a { + .nav-links > li > a { padding: 10px; font-size: 12px; margin-right: 3px; @@ -81,7 +81,7 @@ display: none; } - .center-top-menu, .left-top-menu { + .nav-links, .nav-links { li a { font-size: 14px; padding: 19px 10px; @@ -100,11 +100,6 @@ } @media (max-width: $screen-sm-max) { - .page-with-sidebar .content-wrapper { - padding: 0; - padding-top: 1px; - } - .issues-filters { .milestone-filter, .labels-filter { display: none; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss new file mode 100644 index 00000000000..c537d97fb24 --- /dev/null +++ b/app/assets/stylesheets/framework/nav.scss @@ -0,0 +1,39 @@ +.nav-links { + padding: 0; + margin: 0; + list-style: none; + height: auto; + border-bottom: 1px solid $border-color; + + li { + display: inline-block; + + a { + display: inline-block; + padding: 14px; + padding-top: $gl-padding; + padding-bottom: 11px; + margin-bottom: -1px; + font-size: 15px; + line-height: 28px; + color: #959494; + border-bottom: 2px solid transparent; + + &:hover, &:active, &:focus { + text-decoration: none; + outline: none; + } + } + + &.active a { + color: #000000; + border-bottom: 2px solid #4688f1; + } + + .badge { + font-weight: normal; + background-color: #eee; + color: #78a; + } + } +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index af145191bc8..3ee3443e349 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -3,8 +3,8 @@ .select2-choice { background: #FFF; border-color: #DDD; - height: 42px; - padding: 8px $gl-padding; + height: 36px; + padding: 6px $gl-padding; font-size: $gl-font-size; line-height: 1.42857143; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 83243dd2457..540d0b03163 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -21,11 +21,10 @@ .content-wrapper { width: 100%; - padding: 20px; .container-fluid { background: #FFF; - padding: $gl-padding; + padding: 0 $gl-padding; &.container-blank { background: none; diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 793ab3d9bb9..c4e9f467ce4 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,13 +1,11 @@ .table-holder { - margin: -$gl-padding; - margin-top: 0; - margin-bottom: 0; + margin: 0; } table { &.table { margin-bottom: $gl-padding; - + .dropdown-menu a { text-decoration: none; } @@ -32,6 +30,7 @@ table { } th { + background-color: $background-color; font-weight: normal; font-size: 15px; border-bottom: 1px solid $border-color !important; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index ff41e26ed8a..47b843e5e3d 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -5,10 +5,8 @@ padding: 0; .timeline-entry { - padding: $gl-padding; + padding: $gl-padding 0; border-color: $table-border-color; - margin-left: -$gl-padding; - margin-right: -$gl-padding; color: $gl-gray; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 94f0ed761df..88072606bf5 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -99,47 +99,6 @@ } } -// Nav tabs -.nav.nav-tabs { - margin-bottom: 15px; - - li { - > a { - margin-right: 5px; - line-height: 20px; - border-color: #EEE; - color: #888; - border-bottom: 1px solid #ddd; - .badge { - background-color: #eee; - color: #888; - text-shadow: 0 1px 1px #fff; - } - i.fa { - line-height: 14px; - } - } - &.active { - > a { - border-color: #CCC; - border-bottom: 1px solid #fff; - color: #333; - font-weight: bold; - } - } - } -} - -.nav-tabs > li > a, -.nav-pills > li > a { - color: #666; -} - -.nav-pills > .active > a > span > .badge { - background-color: #fff; - color: $gl-primary; -} - /** * fix to keep tooltips position in top navigation bar diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 63868a34e2a..cd0621cdbf3 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -46,7 +46,7 @@ $font-size-base: $gl-font-size; // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). -$padding-base-vertical: 9px; +$padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; $component-active-color: #fff; $component-active-bg: $brand-info; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 714369d9f15..ab4f71af039 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -177,7 +177,7 @@ body { } .page-title { - margin-top: 0px; + margin-top: $gl-padding; line-height: 1.3; font-size: 1.25em; font-weight: 600; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d0ff3248ce1..85ecdddda79 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -22,6 +22,7 @@ $header-height: 58px; $fixed-layout-width: 1280px; $gl-gray: #5a5a5a; $gl-padding: 16px; +$gl-vert-padding: 6px; $gl-padding-top:10px; $gl-avatar-size: 46px; $secondary-text: #7f8fa4; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 002bd7e8ca5..c3f27333fad 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -4,7 +4,7 @@ position: absolute; top: 0px; right: 4px; - line-height: 40px; + line-height: 56px; } a.js-zen-leave { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 17245d3be7b..e53d6fc6bdc 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -35,6 +35,8 @@ } .commit-box { + border-top: 1px solid $border-color; + .commit-title { margin: 0; font-size: 23px; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index deab805dbc2..529a43548c8 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,7 +1,5 @@ .detail-page-header { - margin: -$gl-padding; - padding: 7px $gl-padding; - margin-bottom: 0px; + padding: 11px 0; border-bottom: 1px solid $border-color; color: #5c5d5e; font-size: 16px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index afd6fb73675..1e2b8b51827 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,9 +1,7 @@ // Common .diff-file { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - border: none; - border-bottom: 1px solid #E7E9EE; + border: 1px solid $border-color; + border-top: none; .diff-header { position: relative; @@ -23,14 +21,6 @@ } } - .diff-controls { - .btn { - padding: 0px 10px; - font-size: 13px; - line-height: 28px; - } - } - .commit-short-id { font-family: $monospace_font; font-size: smaller; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 984b4b91216..8fa15b35748 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,9 +4,7 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px); - margin-left: -$gl-padding; - margin-right: -$gl-padding; + padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px); border-bottom: 1px solid $table-border-color; color: #7f8fa4; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index c3c33f0b76a..977ada0ff38 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -32,10 +32,10 @@ .project-issuable-filter { .controls { float: right; - margin-top: 7px; + margin-top: 11px; } - .center-top-menu { + .nav-links { text-align: left; } } @@ -100,7 +100,7 @@ .cross-project-reference { color: $gl-link-color; - + span { white-space: nowrap; width: 85%; @@ -110,8 +110,13 @@ text-overflow: ellipsis; } + cite { + font-style: normal; + } + button { float: right; + padding: 3px 5px; } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 1e1af662850..ad92cc22815 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -6,7 +6,7 @@ .issue-title { margin-bottom: 5px; font-size: $list-font-size; - font-weight: bold; + font-weight: 600; } .issue-info { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 82effde0bf3..75f2ae80a92 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,9 +3,9 @@ * */ .mr-state-widget { - background: #F7F8FA; + background: $background-color; color: $gl-gray; - border: 1px solid #dce0e6; + border: 1px solid $border-color; @include border-radius(2px); form { @@ -150,7 +150,7 @@ .merge-request-title { margin-bottom: 5px; font-size: $list-font-size; - font-weight: bold; + font-weight: 600; } .merge-request-info { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index d86259f93fb..2c9a42f9892 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -159,6 +159,7 @@ .edit_note { .markdown-area { min-height: 140px; + max-height: 430px; } .note-form-actions { background: transparent; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f24b71963a8..13b0ed769fc 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -26,6 +26,8 @@ } .project-home-panel { + padding-bottom: 40px; + border-bottom: 1px solid $border-color; .cover-controls { .project-settings-dropdown { @@ -51,6 +53,8 @@ } .notifications-btn { + margin-top: -28px; + .fa-bell { margin-right: 6px; } @@ -75,17 +79,6 @@ } } - .git-clone-holder { - max-width: 498px; - - .form-control { - background: #FFF; - font-size: 14px; - height: 42px; - margin-left: -1px; - } - } - .visibility-level-label { @extend .btn; @extend .btn-gray; @@ -98,11 +91,6 @@ } } - .git-clone-holder { - display: inline-table; - position: relative; - } - .project-repo-buttons { margin-top: 12px; margin-bottom: 0px; @@ -112,10 +100,22 @@ margin-bottom: 12px; } + .clone-row { + .split-repo-buttons, + .project-clone-holder { + display: inline-block; + } + + .split-repo-buttons { + margin: 0 12px; + } + } + .btn { @include btn-gray; text-transform: none; } + .count-with-arrow { display: inline-block; position: relative; @@ -160,8 +160,8 @@ border-style: solid; font-size: 13px; font-weight: 600; - line-height: 20px; - padding: 11px 16px; + line-height: 13px; + padding: $gl-vert-padding $gl-padding; letter-spacing: .4px; padding: 10px; text-align: center; @@ -189,117 +189,6 @@ } } -.git-clone-holder { - .project-home-dropdown + & { - margin-right: 45px; - } - - .clone-options { - display: table-cell; - a.btn { - width: 100%; - } - } - - .form-control { - cursor: auto; - @extend .monospace; - background: #FAFAFA; - width: 101%; - } - - .input-group-addon { - background: #f7f8fa; - - &.git-protocols { - padding: 0; - border: none; - - .input-group-btn:last-child > .btn { - @include border-radius-right(0); - - border-left: 1px solid #c6cacf; - margin-left: -2px !important; - } - } - } -} - -.projects-search-form { - - .input-group .form-control { - height: 42px; - } -} - -.input-group-btn { - .btn { - @include btn-gray; - @include btn-middle; - - &:hover { - outline: none; - } - - &:focus { - outline: none; - } - - &:active { - outline: none; - } - - &.btn-clipboard { - padding-left: 15px; - padding-right: 15px; - } - } - - .active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - border: 1px solid #c6cacf !important; - background-color: #e4e7ed !important; - } - - .btn-green { - @include btn-green - } - -} - -.split-repo-buttons { - display: inline-table; - margin: 0 12px 0 12px; - - .btn{ - @include btn-gray; - @include btn-default; - } - - .dropdown-toggle { - margin: -5px; - } -} - -#notification-form { - margin-left: 5px; -} - -.dropdown-new { - margin-left: -5px; -} - -.open > .dropdown-new.btn { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - border: 1px solid #c6cacf !important; - background-color: #e4e7ed !important; - text-transform: none; - color: #313236 !important; - font-size: 15px; -} - .dropdown-menu { @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); @include border-radius (0px); @@ -351,28 +240,6 @@ color: #555; } -ul.nav.nav-projects-tabs { - @extend .nav-tabs; - - padding-left: 8px; - - li { - a { - padding: 6px 25px; - margin-top: 2px; - border-color: #DDD; - background-color: #EEE; - text-shadow: 0 1px 1px white; - color: #555; - } - &.active { - a { - font-weight: bold; - } - } - } -} - .project_member_row form { margin: 0px; } @@ -399,9 +266,9 @@ ul.nav.nav-projects-tabs { .breadcrumb.repo-breadcrumb { padding: 0; - line-height: 42px; background: transparent; border: none; + line-height: 42px; margin: 0; > li + li:before { @@ -416,11 +283,8 @@ ul.nav.nav-projects-tabs { .top-area { border-bottom: 1px solid #EEE; - margin: 0 -16px; - padding: 0 $gl-padding; - height: 42px; - ul.left-top-menu { + ul.nav-links { display: inline-block; width: 50%; margin-bottom: 0px; @@ -431,12 +295,12 @@ ul.nav.nav-projects-tabs { width: 50%; display: inline-block; float: right; - padding-top: 7px; + padding-top: 11px; text-align: right; .btn-green { - margin-top: -2px; margin-left: 10px; + float: right; } } @@ -482,11 +346,11 @@ table.table.protected-branches-list tr.no-border { padding-top: 10px; padding-bottom: 4px; - ul.nav-pills { + ul.nav { display:inline-block; } - .nav-pills li { + .nav li { display:inline; } @@ -523,8 +387,7 @@ pre.light-well { } .projects-search-form { - margin: -$gl-padding; - padding: $gl-padding; + padding: $gl-padding 0; padding-bottom: 0; margin-bottom: 0px; @@ -574,10 +437,8 @@ pre.light-well { @include basic-list; .project-row { - padding: $gl-padding; + padding: $gl-padding 0; border-color: $table-border-color; - margin-left: -$gl-padding; - margin-right: -$gl-padding; &.no-description { .project { @@ -631,8 +492,6 @@ pre.light-well { } .project-last-commit { - margin: 0 7px; - .ci-status { margin-right: 16px; } @@ -662,9 +521,7 @@ pre.light-well { } .project-show-readme .readme-holder { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - padding: ($gl-padding + 7px); + padding: $gl-padding 0; border-top: 0; .edit-project-readme { @@ -672,3 +529,32 @@ pre.light-well { position: relative; } } + +.git-clone-holder { + width: 498px; + + .btn-clipboard { + border: 1px solid $border-color; + padding: 6px $gl-padding; + } + + .project-home-dropdown + & { + margin-right: 45px; + } + + .clone-options { + display: table-cell; + a.btn { + width: 100%; + } + } + + .form-control { + @extend .monospace; + background: #FFF; + font-size: 14px; + margin-left: -1px; + cursor: auto; + width: 101%; + } +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 97505edeabf..6a6dd7dfc85 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,4 +1,7 @@ .tree-holder { + > .nav-block { + margin: 11px 0; + } .file-finder { width: 50%; @@ -13,7 +16,7 @@ tr { > td, > th { - line-height: 28px; + line-height: 26px; } &:hover { @@ -86,12 +89,14 @@ .blob-commit-info { list-style: none; + padding: $gl-padding; + background: $background-color; + border: 1px solid $border-color; + border-bottom: none; margin: 0; - padding: 0; - margin-bottom: 5px; .commit { - padding: $gl-padding 0; + padding: 0; .commit-row-title { .commit-row-message { @@ -115,3 +120,8 @@ font-weight: normal; color: $md-link-color; } + +.tree-controls { + float: right; + margin-top: 11px; +} diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 38814459f66..2eac0cabf7a 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -2,6 +2,7 @@ class AbuseReportsController < ApplicationController def new @abuse_report = AbuseReport.new @abuse_report.user_id = params[:user_id] + @ref_url = params.fetch(:ref_url, '') end def create diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 497c34f8f49..4735b27c65d 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -1,8 +1,12 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController - before_action :broadcast_messages + before_action :finder, only: [:edit, :update, :destroy] def index - @broadcast_message = BroadcastMessage.new + @broadcast_messages = BroadcastMessage.reorder("starts_at ASC").page(params[:page]) + @broadcast_message = BroadcastMessage.new + end + + def edit end def create @@ -15,8 +19,16 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController end end + def update + if @broadcast_message.update(broadcast_message_params) + redirect_to admin_broadcast_messages_path, notice: 'Broadcast Message was successfully updated.' + else + render :edit + end + end + def destroy - BroadcastMessage.find(params[:id]).destroy + @broadcast_message.destroy respond_to do |format| format.html { redirect_back_or_default(default: { action: 'index' }) } @@ -26,14 +38,17 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController protected - def broadcast_messages - @broadcast_messages ||= BroadcastMessage.order("starts_at DESC").page(params[:page]) + def finder + @broadcast_message = BroadcastMessage.find(params[:id]) end def broadcast_message_params - params.require(:broadcast_message).permit( - :alert_type, :color, :ends_at, :font, - :message, :starts_at - ) + params.require(:broadcast_message).permit(%i( + color + ends_at + font + message + starts_at + )) end end diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index e383fe38ea6..79a53556f0a 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController def update if @identity.update_attributes(identity_params) + RepairLdapBlockedUserService.new(@user).execute redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.' else render :edit @@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController def destroy if @identity.destroy + RepairLdapBlockedUserService.new(@user).execute redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.' else redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.' diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d7c927d444c..87f4fb455b8 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController end def unblock - if user.activate + if user.ldap_blocked? + redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab") + elsif user.activate redirect_back_or_admin_user(notice: "Successfully unblocked") else redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked") diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 81cb1367e2c..bf99b2e777d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -115,7 +115,7 @@ class ApplicationController < ActionController::Base # localhost/group/project # if id =~ /\.git\Z/ - redirect_to request.original_url.gsub(/\.git\Z/, '') and return + redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return end project_path = "#{namespace}/#{id}" diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb new file mode 100644 index 00000000000..f159a6d6dc6 --- /dev/null +++ b/app/controllers/projects/artifacts_controller.rb @@ -0,0 +1,56 @@ +class Projects::ArtifactsController < Projects::ApplicationController + layout 'project' + before_action :authorize_read_build_artifacts! + + def download + unless artifacts_file.file_storage? + return redirect_to artifacts_file.url + end + + unless artifacts_file.exists? + return render_404 + end + + send_file artifacts_file.path, disposition: 'attachment' + end + + def browse + return render_404 unless build.artifacts? + + directory = params[:path] ? "#{params[:path]}/" : '' + @entry = build.artifacts_metadata_entry(directory) + + return render_404 unless @entry.exists? + end + + def file + entry = build.artifacts_metadata_entry(params[:path]) + + if entry.exists? + render json: { archive: build.artifacts_file.path, + entry: Base64.encode64(entry.path) } + else + render json: {}, status: 404 + end + end + + private + + def build + @build ||= project.builds.unscoped.find_by!(id: params[:build_id]) + end + + def artifacts_file + @artifacts_file ||= build.artifacts_file + end + + def authorize_read_build_artifacts! + unless can?(current_user, :read_build_artifacts, @project) + if current_user.nil? + return authenticate_user! + else + return render_404 + end + end + end +end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 39d3ba26ba2..92d9699fe84 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_manage_builds!, except: [:index, :show, :status] - before_action :authorize_download_build_artifacts!, only: [:download] layout "project" @@ -43,7 +42,7 @@ class Projects::BuildsController < Projects::ApplicationController def retry unless @build.retryable? - return page_404 + return render_404 end build = Ci::Build.retry(@build) @@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController redirect_to build_path(build) end - def download - unless artifacts_file.file_storage? - return redirect_to artifacts_file.url - end - - unless artifacts_file.exists? - return not_found! - end - - send_file artifacts_file.path, disposition: 'attachment' - end - def status render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) end @@ -79,27 +66,13 @@ class Projects::BuildsController < Projects::ApplicationController @build ||= project.builds.unscoped.find_by!(id: params[:id]) end - def artifacts_file - build.artifacts_file - end - def build_path(build) namespace_project_build_path(build.project.namespace, build.project, build) end def authorize_manage_builds! unless can?(current_user, :manage_builds, project) - return page_404 - end - end - - def authorize_download_build_artifacts! - unless can?(current_user, :download_build_artifacts, @project) - if current_user.nil? - return authenticate_user! - else - return render_404 - end + return render_404 end end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 0aaba3792bf..870f6795219 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -79,7 +79,7 @@ class Projects::CommitController < Projects::ApplicationController def authorize_manage_builds! unless can?(current_user, :manage_builds, project) - return page_404 + return render_404 end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b59b52291fb..68244883803 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -49,7 +49,7 @@ class Projects::IssuesController < Projects::ApplicationController assignee_id: "" ) - @issue = @project.issues.new(issue_params) + @issue = @noteable = @project.issues.new(issue_params) respond_with(@issue) end @@ -61,7 +61,7 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue - @merge_requests = @issue.referenced_merge_requests + @merge_requests = @issue.referenced_merge_requests(current_user) respond_with(@issue) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index de948d271c8..a6284a24223 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -90,6 +90,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute + @noteable = @merge_request @target_branches = if @merge_request.target_project @merge_request.target_project.repository.branch_names diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 2104c7a7a71..92b0caa2efb 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -25,7 +25,7 @@ class Projects::SnippetsController < Projects::ApplicationController end def new - @snippet = @project.snippets.build + @snippet = @noteable = @project.snippets.build end def create diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb new file mode 100644 index 00000000000..7271c933b9b --- /dev/null +++ b/app/controllers/sent_notifications_controller.rb @@ -0,0 +1,25 @@ +class SentNotificationsController < ApplicationController + skip_before_action :authenticate_user! + + def unsubscribe + @sent_notification = SentNotification.for(params[:id]) + return render_404 unless @sent_notification && @sent_notification.unsubscribable? + + noteable = @sent_notification.noteable + noteable.unsubscribe(@sent_notification.recipient) + + flash[:notice] = "You have been unsubscribed from this thread." + if current_user + case noteable + when Issue + redirect_to issue_path(noteable) + when MergeRequest + redirect_to merge_request_path(noteable) + else + redirect_to root_path + end + else + redirect_to new_user_session_path + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 436fbcd4138..f3a2723ee0d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -181,10 +181,6 @@ module ApplicationHelper end end - def broadcast_message - BroadcastMessage.current - end - # Render a `time` element with Javascript-based relative date and tooltip # # time - Time object diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 6484dca6b55..1ed8c710f77 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,16 +1,34 @@ module BroadcastMessagesHelper - def broadcast_styling(broadcast_message) - styling = '' + def broadcast_message(message = BroadcastMessage.current) + return unless message.present? + + content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do + icon('bullhorn') << ' ' << message.message + end + end + + def broadcast_message_style(broadcast_message) + style = '' if broadcast_message.color.present? - styling << "background-color: #{broadcast_message.color}" - styling << '; ' if broadcast_message.font.present? + style << "background-color: #{broadcast_message.color}" + style << '; ' if broadcast_message.font.present? end if broadcast_message.font.present? - styling << "color: #{broadcast_message.font}" + style << "color: #{broadcast_message.font}" end - styling + style + end + + def broadcast_message_status(broadcast_message) + if broadcast_message.active? + 'Active' + elsif broadcast_message.ended? + 'Expired' + else + 'Pending' + end end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index ec0e3f409c1..d6c05843743 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -17,7 +17,7 @@ module ButtonHelper def clipboard_button(data = {}) content_tag :button, icon('clipboard'), - class: 'btn btn-xs btn-clipboard', + class: 'btn btn-clipboard', data: data, type: :button end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index dde83ff36b5..31bf45baeb7 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -27,13 +27,15 @@ module EventsHelper key = key.to_s active = 'active' if @event_filter.active?(key) link_opts = { - class: "event-filter-link btn btn-default #{active}", + class: "event-filter-link", id: "#{key}_event_filter", title: "Filter by #{tooltip.downcase}", } - link_to request.path, link_opts do - content_tag(:span, ' ' + tooltip) + content_tag :li, class: active do + link_to request.path, link_opts do + content_tag(:span, ' ' + tooltip) + end end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index ca41657cec1..1a226252251 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -91,7 +91,7 @@ module GitlabMarkdownHelper def render_wiki_content(wiki_page) case wiki_page.format when :markdown - markdown(wiki_page.content) + markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki) when :asciidoc asciidoc(wiki_page.content) else diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index abdeefed5ef..4a88cb61132 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,31 +1,31 @@ module Emails module Issues def new_issue_email(recipient_id, issue_id) - issue_mail_with_notification(issue_id, recipient_id) do - mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id)) - end + setup_issue_mail(issue_id, recipient_id) + + mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id)) end def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id) - issue_mail_with_notification(issue_id, recipient_id) do - @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) - end + setup_issue_mail(issue_id, recipient_id) + + @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end def closed_issue_email(recipient_id, issue_id, updated_by_user_id) - issue_mail_with_notification(issue_id, recipient_id) do - @updated_by = User.find updated_by_user_id - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) - end + setup_issue_mail(issue_id, recipient_id) + + @updated_by = User.find updated_by_user_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) - issue_mail_with_notification(issue_id, recipient_id) do - @issue_status = status - @updated_by = User.find updated_by_user_id - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) - end + setup_issue_mail(issue_id, recipient_id) + + @issue_status = status + @updated_by = User.find updated_by_user_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end private @@ -38,14 +38,12 @@ module Emails } end - def issue_mail_with_notification(issue_id, recipient_id) + def setup_issue_mail(issue_id, recipient_id) @issue = Issue.find(issue_id) @project = @issue.project @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - yield - - SentNotification.record(@issue, recipient_id, reply_key) + @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) end end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 7923fb770d0..325996e2e16 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -1,77 +1,64 @@ module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id) - @merge_request = MergeRequest.find(merge_request_id) - @project = @merge_request.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request) + setup_merge_request_mail(merge_request_id, recipient_id) + mail_new_thread(@merge_request, from: sender(@merge_request.author_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - - SentNotification.record(@merge_request, recipient_id, reply_key) end def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) - @merge_request = MergeRequest.find(merge_request_id) + setup_merge_request_mail(merge_request_id, recipient_id) + @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - @project = @merge_request.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - - SentNotification.record(@merge_request, recipient_id, reply_key) end def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) - @merge_request = MergeRequest.find(merge_request_id) + setup_merge_request_mail(merge_request_id, recipient_id) + @updated_by = User.find updated_by_user_id - @project = @merge_request.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - - SentNotification.record(@merge_request, recipient_id, reply_key) end def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) - @merge_request = MergeRequest.find(merge_request_id) - @project = @merge_request.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request) + setup_merge_request_mail(merge_request_id, recipient_id) + mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - - SentNotification.record(@merge_request, recipient_id, reply_key) end def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) - @merge_request = MergeRequest.find(merge_request_id) + setup_merge_request_mail(merge_request_id, recipient_id) + @mr_status = status - @project = @merge_request.project @updated_by = User.find updated_by_user_id - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + end + + private + + def setup_merge_request_mail(merge_request_id, recipient_id) + @merge_request = MergeRequest.find(merge_request_id) + @project = @merge_request.project + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) - SentNotification.record(@merge_request, recipient_id, reply_key) + @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) end end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index e1382d2da12..f9650df9a74 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,31 +1,31 @@ module Emails module Notes def note_commit_email(recipient_id, note_id) - note_mail_with_notification(note_id, recipient_id) do - @commit = @note.noteable - @target_url = namespace_project_commit_url(*note_target_url_options) - - mail_answer_thread(@commit, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@commit.title} (#{@commit.short_id})")) - end + setup_note_mail(note_id, recipient_id) + + @commit = @note.noteable + @target_url = namespace_project_commit_url(*note_target_url_options) + + mail_answer_thread(@commit, + from: sender(@note.author_id), + to: recipient(recipient_id), + subject: subject("#{@commit.title} (#{@commit.short_id})")) end def note_issue_email(recipient_id, note_id) - note_mail_with_notification(note_id, recipient_id) do - @issue = @note.noteable - @target_url = namespace_project_issue_url(*note_target_url_options) - mail_answer_thread(@issue, note_thread_options(recipient_id)) - end + setup_note_mail(note_id, recipient_id) + + @issue = @note.noteable + @target_url = namespace_project_issue_url(*note_target_url_options) + mail_answer_thread(@issue, note_thread_options(recipient_id)) end def note_merge_request_email(recipient_id, note_id) - note_mail_with_notification(note_id, recipient_id) do - @merge_request = @note.noteable - @target_url = namespace_project_merge_request_url(*note_target_url_options) - mail_answer_thread(@merge_request, note_thread_options(recipient_id)) - end + setup_note_mail(note_id, recipient_id) + + @merge_request = @note.noteable + @target_url = namespace_project_merge_request_url(*note_target_url_options) + mail_answer_thread(@merge_request, note_thread_options(recipient_id)) end private @@ -42,13 +42,11 @@ module Emails } end - def note_mail_with_notification(note_id, recipient_id) + def setup_note_mail(note_id, recipient_id) @note = Note.find(note_id) @project = @note.project - yield - - SentNotification.record_note(@note, recipient_id, reply_key) + @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key) end end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 3bbdd9cee76..e1cd075a978 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -107,10 +107,9 @@ class Notify < BaseMailer end headers["X-GitLab-#{model.class.name}-ID"] = model.id + headers['X-GitLab-Reply-Key'] = reply_key - if reply_key - headers['X-GitLab-Reply-Key'] = reply_key - + if Gitlab::IncomingEmail.enabled? address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address.display_name = @project.name_with_namespace diff --git a/app/models/ability.rb b/app/models/ability.rb index 5a1a67db8e1..ab59a3506a2 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -160,6 +160,7 @@ class Ability @project_report_rules ||= project_guest_rules + [ :create_commit_status, :read_commit_statuses, + :read_build_artifacts, :download_code, :fork_project, :create_project_snippet, @@ -175,7 +176,6 @@ class Ability :create_merge_request, :create_wiki, :manage_builds, - :download_build_artifacts, :push_code ] end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index ad514706160..61119633717 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -6,7 +6,6 @@ # message :text not null # starts_at :datetime # ends_at :datetime -# alert_type :integer # created_at :datetime # updated_at :datetime # color :string(255) @@ -23,7 +22,22 @@ class BroadcastMessage < ActiveRecord::Base validates :color, allow_blank: true, color: true validates :font, allow_blank: true, color: true + default_value_for :color, '#E75E40' + default_value_for :font, '#FFFFFF' + def self.current - where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last + where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + end + + def active? + started? && !ended? + end + + def started? + Time.zone.now >= starts_at + end + + def ended? + ends_at < Time.zone.now end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a4779d06de8..16a5b03f591 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -30,10 +30,12 @@ # description :string(255) # artifacts_file :text # gl_project_id :integer +# artifacts_metadata :text # module Ci class Build < CommitStatus + include Gitlab::Application.routes.url_helpers LAZY_ATTRIBUTES = ['trace'] belongs_to :runner, class_name: 'Ci::Runner' @@ -49,6 +51,7 @@ module Ci scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } mount_uploader :artifacts_file, ArtifactUploader + mount_uploader :artifacts_metadata, ArtifactUploader acts_as_taggable @@ -125,6 +128,14 @@ module Ci !self.commit.latest_builds_for_ref(self.ref).include?(self) end + def depends_on_builds + # Get builds of the same type + latest_builds = self.commit.builds.similar(self).latest + + # Return builds from previous stages + latest_builds.where('stage_idx < ?', stage_idx) + end + def trace_html html = Ci::Ansi2html::convert(trace) if trace.present? html || '' @@ -291,21 +302,18 @@ module Ci end def target_url - Gitlab::Application.routes.url_helpers. - namespace_project_build_url(project.namespace, project, self) + namespace_project_build_url(project.namespace, project, self) end def cancel_url if active? - Gitlab::Application.routes.url_helpers. - cancel_namespace_project_build_path(project.namespace, project, self) + cancel_namespace_project_build_path(project.namespace, project, self) end end def retry_url if retryable? - Gitlab::Application.routes.url_helpers. - retry_namespace_project_build_path(project.namespace, project, self) + retry_namespace_project_build_path(project.namespace, project, self) end end @@ -321,20 +329,35 @@ module Ci pending? && !any_runners_online? end - def download_url - if artifacts_file.exists? - Gitlab::Application.routes.url_helpers. - download_namespace_project_build_path(project.namespace, project, self) - end - end - def execute_hooks build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) end + def artifacts? + artifacts_file.exists? + end + + def artifacts_download_url + if artifacts? + download_namespace_project_build_artifacts_path(project.namespace, project, self) + end + end + + def artifacts_browse_url + if artifacts_browser_supported? + browse_namespace_project_build_artifacts_path(project.namespace, project, self) + end + end + + def artifacts_browser_supported? + artifacts? && artifacts_metadata.exists? + end + def artifacts_metadata_entry(path) + Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry + end private diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index bb98cd5c7da..2b9a457c8ab 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -33,6 +33,10 @@ module Ci trigger_requests.last end + def last_used + last_trigger_request.try(:created_at) + end + def short_token token[0...10] end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 7f6f497f325..e786bd7dd93 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -18,8 +18,12 @@ module Ci belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - validates_presence_of :key validates_uniqueness_of :key, scope: :gl_project_id + validates :key, + presence: true, + length: { within: 0..255 }, + format: { with: /\A[a-zA-Z0-9_]+\z/, + message: "can contain only letters, digits and '_'." } attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ff479493474..66e0502fc0c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -56,6 +56,8 @@ class CommitStatus < ActiveRecord::Base scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } + AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled'] + state_machine :status, initial: :pending do event :run do transition pending: :running @@ -131,7 +133,11 @@ class CommitStatus < ActiveRecord::Base false end - def download_url + def artifacts_download_url + nil + end + + def artifacts_browse_url nil end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 18a00f95b48..04650a9e67a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -119,6 +119,12 @@ module Issuable update(subscribed: !subscribed?(user)) end + def unsubscribe(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: false) + end + def to_hook_data(user) { object_kind: self.class.name.underscore, diff --git a/app/models/identity.rb b/app/models/identity.rb index 8bcdc194953..e1915b079d4 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base validates :provider, presence: true validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider } + + def ldap? + provider.starts_with?('ldap') + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index f52e47f3e62..7beba984608 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -85,10 +85,10 @@ class Issue < ActiveRecord::Base reference end - def referenced_merge_requests + def referenced_merge_requests(current_user = nil) Gitlab::ReferenceExtractor.lazily do [self, *notes].flat_map do |note| - note.all_references.merge_requests + note.all_references(current_user).merge_requests end end.sort_by(&:iid) end diff --git a/app/models/note.rb b/app/models/note.rb index 3d5b663c99f..3e1375e5ad6 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -358,6 +358,10 @@ class Note < ActiveRecord::Base !system? && !is_award end + def cross_reference_not_visible_for?(user) + cross_reference? && referenced_mentionables(user).empty? + end + # Checks if note is an award added as a comment # # If note is an award, this method sets is_award to true diff --git a/app/models/project.rb b/app/models/project.rb index 31990485f7d..7e131151513 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -397,7 +397,7 @@ class Project < ActiveRecord::Base result.password = '*****' unless result.password.nil? result.to_s rescue - original_url + self.import_url end def check_limit diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index b5fec38378b..8ce47495971 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -38,6 +38,10 @@ class ProjectWiki [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') end + def wiki_base_path + ["/", @project.path_with_namespace, "/wikis"].join('') + end + # Returns the Gollum::Wiki object. def wiki @wiki ||= begin diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index f36eda1531b..77115597d71 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base class << self def reply_key - return nil unless Gitlab::IncomingEmail.enabled? - SecureRandom.hex(16) end @@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base def record_note(note, recipient_id, reply_key, params = {}) params[:line_code] = note.line_code - + record(note.noteable, recipient_id, reply_key, params) end end + def unsubscribable? + !for_commit? + end + def for_commit? noteable_type == "Commit" end @@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base super end end + + def to_param + self.reply_key + end end diff --git a/app/models/user.rb b/app/models/user.rb index 46b36c605b0..592468933ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -196,10 +196,22 @@ class User < ActiveRecord::Base state_machine :state, initial: :active do event :block do transition active: :blocked + transition ldap_blocked: :blocked + end + + event :ldap_block do + transition active: :ldap_blocked end event :activate do transition blocked: :active + transition ldap_blocked: :active + end + + state :blocked, :ldap_blocked do + def blocked? + true + end end end @@ -207,7 +219,7 @@ class User < ActiveRecord::Base # Scopes scope :admins, -> { where(admin: true) } - scope :blocked, -> { with_state(:blocked) } + scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index e9413c34bae..2a65f0431c4 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -169,7 +169,7 @@ class WikiPage private def set_attributes - attributes[:slug] = @page.escaped_url_path + attributes[:slug] = @page.url_path attributes[:title] = @page.title attributes[:format] = @page.format end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index cabc3d8fabb..e8bef250d8b 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -44,7 +44,7 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) - if params[:should_remove_source_branch] + if params[:should_remove_source_branch].present? DeleteBranchService.new(@merge_request.source_project, current_user). execute(merge_request.source_branch) end diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb new file mode 100644 index 00000000000..863cef7ff61 --- /dev/null +++ b/app/services/repair_ldap_blocked_user_service.rb @@ -0,0 +1,17 @@ +class RepairLdapBlockedUserService + attr_accessor :user + + def initialize(user) + @user = user + end + + def execute + user.block if ldap_hard_blocked? + end + + private + + def ldap_hard_blocked? + user.ldap_blocked? && !user.ldap_user? + end +end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 1b0ae6c0056..1cd93263c9f 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -32,6 +32,10 @@ class ArtifactUploader < CarrierWave::Uploader::Base self.class.storage == CarrierWave::Storage::File end + def filename + file.try(:filename) + end + def exists? file.try(:exists?) end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index 3e5cdd2ce4a..f125ecf7be5 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -16,7 +16,7 @@ .form-group = f.label :message, class: 'control-label' .col-sm-10 - = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true + = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true, value: sanitize(@ref_url) .help-block Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml new file mode 100644 index 00000000000..953b8b69368 --- /dev/null +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -0,0 +1,37 @@ +.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } + = icon('bullhorn') + %span= @broadcast_message.message || "Your message here" + += form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-requires-input'} do |f| + -if @broadcast_message.errors.any? + .alert.alert-danger + - @broadcast_message.errors.full_messages.each do |msg| + %p= msg + .form-group + = f.label :message, class: 'control-label' + .col-sm-10 + = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true + .form-group.js-toggle-colors-container + .col-sm-10.col-sm-offset-2 + = link_to 'Customize colors', '#', class: 'js-toggle-colors-link' + .form-group.js-toggle-colors-container.hide + = f.label :color, "Background Color", class: 'control-label' + .col-sm-10 + = f.color_field :color, class: "form-control" + .form-group.js-toggle-colors-container.hide + = f.label :font, "Font Color", class: 'control-label' + .col-sm-10 + = f.color_field :font, class: "form-control" + .form-group + = f.label :starts_at, class: 'control-label' + .col-sm-10.datetime-controls + = f.datetime_select :starts_at, {}, class: 'form-control form-control-inline' + .form-group + = f.label :ends_at, class: 'control-label' + .col-sm-10.datetime-controls + = f.datetime_select :ends_at, {}, class: 'form-control form-control-inline' + .form-actions + - if @broadcast_message.persisted? + = f.submit "Update broadcast message", class: "btn btn-create" + - else + = f.submit "Add broadcast message", class: "btn btn-create" diff --git a/app/views/admin/broadcast_messages/edit.html.haml b/app/views/admin/broadcast_messages/edit.html.haml new file mode 100644 index 00000000000..45e053eb31d --- /dev/null +++ b/app/views/admin/broadcast_messages/edit.html.haml @@ -0,0 +1,3 @@ +- page_title "Broadcast Messages" + += render 'form' diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 17dffebd360..49e33698b63 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -1,60 +1,37 @@ - page_title "Broadcast Messages" + %h3.page-title Broadcast Messages %p.light - Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more. -.broadcast-message-preview - %i.fa.fa-bullhorn - %span Your message here - -= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal'} do |f| - -if @broadcast_message.errors.any? - .alert.alert-danger - - @broadcast_message.errors.full_messages.each do |msg| - %p= msg - .form-group - = f.label :message, class: 'control-label' - .col-sm-10 - = f.text_area :message, class: "form-control", rows: 2, required: true - %div - = link_to '#', class: 'js-toggle-colors-link' do - Customize colors - .form-group.js-toggle-colors-container.hide - = f.label :color, "Background Color", class: 'control-label' - .col-sm-10 - = f.color_field :color, value: "#eb9532", class: "form-control" - .form-group.js-toggle-colors-container.hide - = f.label :font, "Font Color", class: 'control-label' - .col-sm-10 - = f.color_field :font, value: "#FFFFFF", class: "form-control" - .form-group - = f.label :starts_at, class: 'control-label' - .col-sm-10.datetime-controls - = f.datetime_select :starts_at - .form-group - = f.label :ends_at, class: 'control-label' - .col-sm-10.datetime-controls - = f.datetime_select :ends_at - .form-actions - = f.submit "Add broadcast message", class: "btn btn-create" + Broadcast messages are displayed for every user and can be used to notify + users about scheduled maintenance, recent upgrades and more. --if @broadcast_messages.any? - %ul.bordered-list.broadcast-messages - - @broadcast_messages.each do |broadcast_message| - %li - .pull-right - - if broadcast_message.starts_at - %strong - #{broadcast_message.starts_at.to_s(:short)} - \... - - if broadcast_message.ends_at - %strong - #{broadcast_message.ends_at.to_s(:short)} - - = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do - %i.fa.fa-times.cred += render 'form' - .message= broadcast_message.message +%br.clearfix +-if @broadcast_messages.any? + %table.table + %thead + %tr + %th Status + %th Preview + %th Starts + %th Ends + %th + %tbody + - @broadcast_messages.each do |message| + %tr + %td + = broadcast_message_status(message) + %td + = broadcast_message(message) + %td + = message.starts_at + %td + = message.ends_at + %td + = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs' + = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger' = paginate @broadcast_messages diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 6936e614346..c395bd908c3 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -60,8 +60,8 @@ %td .pull-right - - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url - = link_to build.download_url, title: 'Download artifacts' do + - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts? + = link_to build.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, build.project) - if build.active? diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index ddd4e1481eb..ebf2b7b60e7 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -4,7 +4,7 @@ - if @all_builds.running_or_pending.any? = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - %ul.center-top-menu + %ul.nav-links %li{class: ('active' if @scope.nil?)} = link_to admin_builds_path do All diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 1484baa78e0..af9fdeb0734 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,12 +1,13 @@ - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger] -%ul.nav.nav-tabs.log-tabs +%ul.nav-links.log-tabs - loggers.each do |klass| %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' -%p.light To prevent performance issues admin logs output the last 2000 lines +.gray-content-block + To prevent performance issues admin logs output the last 2000 lines .tab-content - loggers.each do |klass| .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index bc44b1b1d8e..ce5e21e54cc 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -12,7 +12,7 @@ %i.fa.fa-pencil-square-o Edit %hr -%ul.nav.nav-tabs +%ul.nav-links = nav_link(path: 'users#show') do = link_to "Account", admin_user_path(@user) = nav_link(path: 'users#groups') do @@ -23,3 +23,4 @@ = link_to "SSH keys", keys_admin_user_path(@user) = nav_link(controller: :identities) do = link_to "Identities", admin_user_identities_path(@user) +.append-bottom-default diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index a92c9c152b9..b050a4d01c3 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,101 +1,101 @@ - page_title "Users" = render 'shared/show_aside' -.row - %aside.col-md-3 - .admin-filter - %ul.nav.nav-pills.nav-stacked - %li{class: "#{'active' unless params[:filter]}"} - = link_to admin_users_path do - Active - %small.pull-right= number_with_delimiter(User.active.count) - %li{class: "#{'active' if params[:filter] == "admins"}"} - = link_to admin_users_path(filter: "admins") do - Admins - %small.pull-right= number_with_delimiter(User.admins.count) - %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.pull-right= number_with_delimiter(User.with_two_factor.count) - %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.pull-right= number_with_delimiter(User.without_two_factor.count) - %li{class: "#{'active' if params[:filter] == "blocked"}"} - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.pull-right= number_with_delimiter(User.blocked.count) - %li{class: "#{'active' if params[:filter] == "wop"}"} - = link_to admin_users_path(filter: "wop") do - Without projects - %small.pull-right= number_with_delimiter(User.without_projects.count) - %hr - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false - = hidden_field_tag "filter", params[:filter] - = button_tag class: 'btn btn-primary' do - %i.fa.fa-search - %hr - = link_to 'Reset', admin_users_path, class: "btn btn-cancel" +.admin-filter + %ul.nav-links + %li{class: "#{'active' unless params[:filter]}"} + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + %li{class: "#{'active' if params[:filter] == "admins"}"} + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + %li{class: "#{'active' if params[:filter] == "blocked"}"} + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + %li{class: "#{'active' if params[:filter] == "wop"}"} + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) - %section.col-md-9 - .panel.panel-default - .panel-heading - Users (#{number_with_delimiter(@users.total_count)}) - .panel-head-actions - .dropdown.inline - %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_name - %b.caret - %ul.dropdown-menu - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated - - = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" - %ul.well-list - - @users.each do |user| + .gray-content-block.second-block + .pull-right + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_name + %b.caret + %ul.dropdown-menu %li - .list-item-name - - if user.blocked? - %i.fa.fa-lock.cred + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated + + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = form_tag admin_users_path, method: :get, class: 'form-inline' do + .form-group + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false + = hidden_field_tag "filter", params[:filter] + = button_tag class: 'btn btn-primary' do + %i.fa.fa-search + + +.panel.panel-default + %ul.well-list + - @users.each do |user| + %li + .list-item-name + - if user.blocked? + %i.fa.fa-lock.cred + - else + %i.fa.fa-user.cgreen + = link_to user.name, [:admin, user] + - if user.admin? + %strong.cred (Admin) + - if user == current_user + %span.cred It's you! + .pull-right + %span.light + %i.fa.fa-envelope + = mail_to user.email, user.email, class: 'light' + + .pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' + - unless user == current_user + - if user.ldap_blocked? + = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do + %i.fa.fa-lock + Unblock + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' - else - %i.fa.fa-user.cgreen - = link_to user.name, [:admin, user] - - if user.admin? - %strong.cred (Admin) - - if user == current_user - %span.cred It's you! - .pull-right - %span.light - %i.fa.fa-envelope - = mail_to user.email, user.email, class: 'light' - - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - - unless user == current_user - - if user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning" - - if user.access_locked? - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" - = paginate @users, theme: "gitlab" + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' + - if user.access_locked? + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' += paginate @users, theme: "gitlab" diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index f98fd9f06ba..dc76599b776 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,9 +1,9 @@ .hidden-xs = render "events/event_last_push", event: @last_push -.gray-content-block +.nav-block - if current_user - .pull-right + .controls = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index 9f4be025bf2..b78e70ebc1e 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links %li{ class: ("active" unless params[:filter]) } = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 64bd356f546..6ca97a692b4 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(page: dashboard_groups_path) do = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do Your Groups diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index f4a3e3162bf..5c4b58cd688 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,7 +1,7 @@ = content_for :flash_message do = render 'shared/project_limit' .top-area - %ul.left-top-menu + %ul.nav-links = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index 0ae62d6f1b6..b25e8ea1f0c 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do Your Snippets diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 49a558e8ac9..3810267577c 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -48,7 +48,7 @@ #{@milestone.open_items_count} open = milestone_progress_bar(@milestone) -%ul.center-top-menu.no-top.no-bottom +%ul.nav-links.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 07b6d57932e..d4e7862981c 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -3,32 +3,36 @@ = render 'dashboard/snippets_head' -.gray-content-block - .pull-right +.nav-block + .controls = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do = icon('plus') New Snippet - .btn-group.btn-group-next.snippet-scope-menu - = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do - All - %span.badge - = current_user.snippets.count - - = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do - Private - %span.badge - = current_user.snippets.are_private.count - - = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do - Internal - %span.badge - = current_user.snippets.are_internal.count - - = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do - Public - %span.badge - = current_user.snippets.are_public.count + .nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to dashboard_snippets_path do + All + %span.badge + = current_user.snippets.count + + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to dashboard_snippets_path(scope: 'are_private') do + Private + %span.badge + = current_user.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to dashboard_snippets_path(scope: 'are_internal') do + Internal + %span.badge + = current_user.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to dashboard_snippets_path(scope: 'are_public') do + Public + %span.badge + = current_user.snippets.are_public.count = render 'snippets/snippets' diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 41ad2c231d4..2c15e2c4891 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -7,7 +7,7 @@ %h3 Sign in .login-body - if form_based_providers.any? - %ul.nav.nav-tabs + %ul.nav-links - if crowd_enabled? %li.active = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 7e3e2e28bc9..e2f97fd9337 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,7 +1,7 @@ - header_title group_title(@group, "Settings", edit_group_path(@group)) - @blank_container = true -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading Group settings .panel-body diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 335bf036074..6a8acc42af9 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -2,7 +2,7 @@ - header_title group_title(@group, "Members", group_group_members_path(@group)) - @blank_container = true -.group-members-page +.group-members-page.prepend-top-default - if current_user && current_user.can?(:admin_group_member, @group) .panel.panel-default .panel-heading diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index d063b257b5e..1233da85524 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -54,7 +54,7 @@ #{@milestone.open_items_count} open = milestone_progress_bar(@milestone) -%ul.center-top-menu.no-top.no-bottom +%ul.nav-links.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index f1d507a50c7..9ca11ed1177 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,7 +1,7 @@ - page_title "Projects" - header_title group_title(@group, "Projects", projects_group_path(@group)) -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading %strong= @group.name projects: diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 48a544fc834..ebb3df7dca3 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,5 @@ +- @no_container = true + - unless can?(current_user, :read_group, @group) - @disable_search_panel = true @@ -25,8 +27,8 @@ .cover-desc.description = markdown(@group.description, pipeline: :description) -- if can?(current_user, :read_group, @group) - %ul.center-top-menu.no-top + + %ul.nav-links %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity @@ -35,20 +37,22 @@ = link_to "#projects", 'data-toggle' => 'tab' do Projects - .tab-content - .tab-pane.active#activity - .gray-content-block.activity-filter-block - - if current_user - = render "events/event_last_push", event: @last_push +- if can?(current_user, :read_group, @group) + %div{ class: container_class } + .tab-content + .tab-pane.active#activity + .activity-filter-block + - if current_user + = render "events/event_last_push", event: @last_push - = render 'shared/event_filter' + = render 'shared/event_filter' - .content_list - = spinner + .content_list + = spinner - .tab-pane#projects - = render "projects", projects: @projects + .tab-pane#projects + = render "projects", projects: @projects - else - %p.center-top-menu.no-top + %p.nav-links.no-top No projects to show diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index d9ffda884c8..7b45bd09050 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -139,26 +139,9 @@ %h2#navs Navigation %h4 - %code .center-top-menu + %code .nav-links .example - %ul.center-top-menu - %li.active - %a Open - %li - %a Closed - - %h4 - %code .btn-group.btn-group-next - .example - %div.btn-group.btn-group-next - %a.btn.active Open - %a.btn Closed - - - %h4 - %code .nav.nav-tabs - .example - %ul.nav.nav-tabs + %ul.nav-links %li.active %a Open %li diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml index e7d477c225e..3a7e0929c16 100644 --- a/app/views/layouts/_broadcast.html.haml +++ b/app/views/layouts/_broadcast.html.haml @@ -1,4 +1 @@ -- if broadcast_message.present? - .broadcast-message{ style: broadcast_styling(broadcast_message) } - %i.fa.fa-bullhorn - = broadcast_message.message += broadcast_message diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index ec7cd79bc54..26159989777 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -24,7 +24,7 @@ .content-wrapper = render "layouts/flash" = yield :flash_message - %div{ class: container_class } + %div{ class: (container_class unless @no_container) } .content .clearfix = yield diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 3ca4c340406..325c68c69dc 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -44,6 +44,10 @@ %br -# Don't link the host is the line below, one link in the email is easier to quickly click than two. You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. - If you'd like to receive fewer emails, you can adjust your notification settings. + If you'd like to receive fewer emails, you can + - if @sent_notification && @sent_notification.unsubscribable? + = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification) + from this thread or + adjust your notification settings. - = email_action @target_url
\ No newline at end of file + = email_action @target_url diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 17e47c622ce..a42fd38de3a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -6,7 +6,7 @@ .alert.alert-info Some options are unavailable for LDAP accounts -.account-page +.account-page.prepend-top-default .panel.panel-default.update-token .panel-heading Reset Private token diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 101880bd105..961b61d2e76 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,6 +1,6 @@ -.gray-content-block.activity-filter-block +.nav-block.activity-filter-block - if current_user - .pull-right + .controls = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do %i.fa.fa-rss diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index fa978325ddd..96c2fa87f45 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -1,5 +1,5 @@ #tree-holder.tree-holder.clearfix - .gray-content-block.second-block + .nav-block = render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_content', tree: @tree diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 53eec76129b..298c6664997 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -44,13 +44,16 @@ = render 'projects/buttons/star' = render 'projects/buttons/fork' - = render "shared/clone_panel" + .clone-row + .project-clone-holder + = render "shared/clone_panel" - .split-repo-buttons - = render "projects/buttons/download" - = render 'projects/buttons/dropdown' + .split-repo-buttons + .btn-group.pull-left + = render "projects/buttons/download" + = render 'projects/buttons/dropdown' - = render 'projects/buttons/notifications' + = render 'projects/buttons/notifications' :javascript new Star(); diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 54c818baaf4..1fb37ef6621 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,6 +1,6 @@ .md-area .md-header.clearfix - %ul.center-top-menu + %ul.nav-links %li.active %a.js-md-write-button(href="#md-write-holder" tabindex="-1") Write diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index d5829568275..e701253d7de 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,6 +1,6 @@ .zennable .zen-backdrop - - classes << ' js-gfm-input markdown-area' + - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f = f.text_area attr, class: classes - else diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml new file mode 100644 index 00000000000..5b87d55efd5 --- /dev/null +++ b/app/views/projects/artifacts/_tree_directory.html.haml @@ -0,0 +1,7 @@ +%tr{ class: 'tree-item' } + %td.tree-item-file-name + = tree_icon('folder', '755', directory.name) + %span.str-truncated + = link_to directory.name, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path) + %td + %td diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml new file mode 100644 index 00000000000..92c1648f726 --- /dev/null +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -0,0 +1,11 @@ +%tr{ class: 'tree-item' } + %td.tree-item-file-name + = tree_icon('file', '664', file.name) + %span.str-truncated + = file.name + %td + = number_to_human_size(file.metadata[:size], precision: 2) + %td + = link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path), + class: 'btn btn-xs btn-default artifact-download' do + = icon('download') diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml new file mode 100644 index 00000000000..1add7ef6bfb --- /dev/null +++ b/app/views/projects/artifacts/browse.html.haml @@ -0,0 +1,24 @@ +- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' += render 'projects/builds/header_title' + +#tree-holder.tree-holder + .gray-content-block.top-block.clearfix + .pull-right + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), + class: 'btn btn-default' do + = icon('download') + Download artifacts archive + +%div.tree-content-holder + .table-holder + %table.table.tree-table.table-striped + %thead + %tr + %th Name + %th Size + %th Download + = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory + = render partial: 'tree_file', collection: @entry.files, as: :file + +- if @entry.empty? + .center Empty diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 2a3315da3db..3d8d88834e2 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.top-block +.nav-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'blob', path: @path diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 09fa148b129..a279e6eda55 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,7 +2,7 @@ = render "header_title" .file-editor - %ul.center-top-menu.no-bottom.js-edit-mode + %ul.nav-links.no-bottom.js-edit-mode %li.active = link_to '#editor' do = icon('edit') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index a234536723e..76a823d3828 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,12 +1,12 @@ - commit = @repository.commit(branch.target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 -- diverging_commit_counts = @repository.diverging_commit_counts(branch) +- diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do - %strong.str-truncated= branch.name + %span.item-title.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-primary default diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 3bbfdb1e3b0..5d18c0d803a 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -8,7 +8,7 @@ - if @all_builds.running_or_pending.any? = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - %ul.center-top-menu + %ul.nav-links %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do All diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 5b7ecce86ab..2be572d3b10 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -14,7 +14,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) - %ul.center-top-menu.no-top.no-bottom + %ul.nav-links.no-top.no-bottom - @commit.latest_builds_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do @@ -89,9 +89,15 @@ Test coverage %h1 #{@build.coverage}% - - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url - .build-widget.center - = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary' + - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts? + + .build-widget.artifacts + %h4.title Build artifacts + .center + .btn-group{ role: :group } + = link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary' + - if @build.artifacts_browser_supported? + = link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' .build-widget %h4.title diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index f9ab78e7874..511863d774e 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user - %span.dropdown - %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"} + .btn-group + %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can?(current_user, :create_issue, @project) diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index f74f8b427ec..ea33aa472a6 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu.no-top.no-bottom.commit-ci-menu +%ul.nav-links.no-top.no-bottom.commit-ci-menu = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index ddb77fd796b..bbe820b8842 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -50,7 +50,7 @@ .commit-info-row.branches %i.fa.fa-spinner.fa-spin -.commit-box.gray-content-block.middle-block +.commit-box.content-block %h3.commit-title = markdown escape_once(@commit.title), pipeline: :single_line - if @commit.description.present? diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 99d62503a94..7118a4846c6 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,6 +1,7 @@ - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" = render "projects/commits/header_title" -= render "commit_box" +.prepend-top-default + = render "commit_box" = render "ci_menu" = render "builds" diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 58aa45e8d2c..02297158dec 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -2,7 +2,9 @@ - page_description @commit.description = render "projects/commits/header_title" -= render "commit_box" + +.prepend-top-default + = render "commit_box" - if @ci_commit = render "ci_menu" - else diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 74a05df24d3..1736dccaf3c 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -66,8 +66,8 @@ %td .pull-right - - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url - = link_to commit_status.download_url, title: 'Download artifacts' do + - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts? + = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, commit_status.project) - if commit_status.active? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 012825f0fdb..7f2903589a9 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -11,7 +11,7 @@ = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } .commit-row-title - %strong.str-truncated + %span.item-title.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index fcccb002d7e..498c5e05b32 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 034057da42e..ede64d47ab3 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -6,7 +6,7 @@ = render "head" -.gray-content-block +.gray-content-block.second-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f9d661d59d2..f67058ae0ba 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - diff_files = safe_diff_files(diffs) -.gray-content-block.middle-block.oneline-block +.content-block.oneline-block .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 31e752c6649..8a99aceef7f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,6 +1,6 @@ - @blank_container = true -.project-edit-container +.project-edit-container.prepend-top-default .project-edit-errors .project-edit-content .panel.panel-default diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 503d156661e..b34d106d565 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,3 +1,5 @@ +- @no_container = true + = content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' @@ -17,40 +19,41 @@ file to this project. - if can?(current_user, :download_code, @project) - .prepend-top-20 - .empty_wrapper - %h3.page-title-empty - Command line instructions - %div.git-empty - %fieldset - %h5 Git global setup - %pre.light-well - :preserve - git config --global user.name "#{h git_user_name}" - git config --global user.email "#{h git_user_email}" + %div{ class: container_class } + .prepend-top-20 + .empty_wrapper + %h3.page-title-empty + Command line instructions + %div.git-empty + %fieldset + %h5 Git global setup + %pre.light-well + :preserve + git config --global user.name "#{h git_user_name}" + git config --global user.email "#{h git_user_email}" - %fieldset - %h5 Create a new repository - %pre.light-well - :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} - cd #{h @project.path} - touch README.md - git add README.md - git commit -m "add README" - git push -u origin master + %fieldset + %h5 Create a new repository + %pre.light-well + :preserve + git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + cd #{h @project.path} + touch README.md + git add README.md + git commit -m "add README" + git push -u origin master - %fieldset - %h5 Existing folder or Git repository - %pre.light-well - :preserve - cd existing_folder - git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} - git add . - git commit - git push -u origin master + %fieldset + %h5 Existing folder or Git repository + %pre.light-well + :preserve + cd existing_folder + git init + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit + git push -u origin master - - if can? current_user, :remove_project, @project - .prepend-top-20 - = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + - if can? current_user, :remove_project, @project + .prepend-top-20 + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index a47643bd09c..79a56647c53 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index a14943b15d3..dd2c59e112a 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -18,7 +18,7 @@ = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %ul.merge-request-tabs.nav-links.no-top.no-bottom %li.commits-tab = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 095876450a0..200bfa5ac4f 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -45,7 +45,7 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits.present? - %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %ul.merge-request-tabs.nav-links.no-top.no-bottom %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index d6a44c9f0a1..67d95ab0364 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -21,10 +21,11 @@ = render 'shared/milestone_expired', milestone: milestone .col-sm-6 - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do - %i.fa.fa-pencil-square-o + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do + = icon('pencil-square-o') Edit + \ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do - %i.fa.fa-trash-o + = icon('trash-o') Delete diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 1670ea8741a..1142c584592 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -20,16 +20,16 @@ .pull-right - if can?(current_user, :admin_milestone, @project) - if @milestone.active? - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" - else - = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do - %i.fa.fa-trash-o + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr btn-remove" do + = icon('trash-o') Delete - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do + = icon('pencil-square-o') Edit .detail-page-description.gray-content-block.second-block @@ -57,7 +57,7 @@ %span.pull-right= @milestone.expires_at = milestone_progress_bar(@milestone) -%ul.center-top-menu.no-top.no-bottom +%ul.nav-links.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index ca60dd239b2..62db86fb181 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -2,10 +2,14 @@ - @discussions.each do |discussion_notes| - note = discussion_notes.first - if note_for_main_target?(note) + - next if note.cross_reference_not_visible_for?(current_user) + = render discussion_notes - else = render 'projects/notes/discussion', discussion_notes: discussion_notes - else - @notes.each do |note| - next unless note.author + - next if note.cross_reference_not_visible_for?(current_user) + = render note diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 29225a36364..6239a148905 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -2,7 +2,7 @@ = render "header_title" - @blank_container = true -.project-members-page +.project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 315afe4a764..2d5b9f43c24 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -1,5 +1,6 @@ - page_title "Runners" -.light + +.light.prepend-top-default %p A 'runner' is a process which runs a build. You can setup as many runners as you need. diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8436be433b1..4310f038fc9 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,3 +1,5 @@ +- @no_container = true + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") @@ -8,11 +10,10 @@ = render 'shared/no_password' = render 'projects/last_push' - = render "home_panel" .project-stats.gray-content-block.second-block - %ul.nav.nav-pills + %ul.nav %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = pluralize(number_with_delimiter(@project.commit_count), 'commit') @@ -57,15 +58,17 @@ = link_to add_contribution_guide_path(@project) do Add Contribution guide -- if @project.archived? - .text-warning.center.prepend-top-20 - %p - = icon("exclamation-triangle fw") - Archived project! Repository is read-only - - if @repository.commit .content-block.second-block.white - = render 'projects/last_commit', commit: @repository.commit, project: @project + %div{ class: container_class } + = render 'projects/last_commit', commit: @repository.commit, project: @project + +%div{ class: container_class } + - if @project.archived? + .text-warning.center.prepend-top-20 + %p + = icon("exclamation-triangle fw") + Archived project! Repository is read-only -%div{class: "project-show-#{default_project_view}"} - = render default_project_view + %div{class: "project-show-#{default_project_view}"} + = render default_project_view diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 28b706c5c7e..399782273d3 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -3,7 +3,7 @@ %li %div = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do - %strong + %span.item-title = icon('tag') = tag.name - if tag.message.present? diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index b594d4f1f27..8c7f93f93b6 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -18,7 +18,7 @@ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o .title - %strong= @tag.name + %span.item-title= @tag.name - if @tag.message.present? %span.light diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 1927883513a..558e6146ae9 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,6 +1,6 @@ %div.tree-content-holder .table-holder - %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } + %table.table#tree-slider{class: "table_#{@hex_path} tree-table" } %thead %tr %th Name diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index c57570afa09..91fb2a44594 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -5,13 +5,13 @@ = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' -.pull-right +.tree-controls = render 'projects/find_file_link' - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true #tree-holder.tree-holder.clearfix - .gray-content-block.top-block + .nav-block = render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_content', tree: @tree diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index e6e6ad5bc4b..69ba301e231 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -7,7 +7,7 @@ = render 'projects/wikis/new' - %ul.center-top-menu + %ul.nav-links = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index f0547e9c057..53b37b1104e 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -5,12 +5,9 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title New Wiki Page .modal-body - = label_tag :new_wiki_path do - %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) - %p.hidden.text-danger{data: { error: "slug" }} - The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and / - %p.hint - Please don't use spaces. + .form-group + = label_tag :new_wiki_path do + %span Page slug + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) .form-actions = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 11c8c4f0eba..dd27ea2b11b 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -3,14 +3,12 @@ = render 'nav' .gray-content-block - .row - .col-sm-6 - %h3.page-title.oneline - Git access for - %strong= @project_wiki.path_with_namespace + %span.oneline + Git access for + %strong= @project_wiki.path_with_namespace - .col-sm-6 - = render "shared/clone_panel", project: @project_wiki + .pull-right + = render "shared/clone_panel", project: @project_wiki .git-empty.prepend-top-default %fieldset diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 481451edb23..2c3fca439f3 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs.search-filter +%ul.nav-links.search-filter - if @project %li{class: ("active" if @scope == 'blobs')} = link_to search_filter_path(scope: 'blobs') do diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 2a38c98dcfc..d0e64537621 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,7 +1,7 @@ - if @search_results.empty? = render partial: "search/results/empty" - else - %p.light + .gray-content-block Search results for %code = @search_term diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index f4f3dcfc29f..215dbb3909e 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,5 +1,7 @@ - page_title @search_term -= render 'search/form' + +.prepend-top-default + = render 'search/form' - if @search_term = render 'search/category' = render 'search/results' diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 687a59c270f..faf7e49ed29 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,7 +1,7 @@ - project = project || @project -.git-clone-holder - .btn-group.clone-options +.git-clone-holder.input-group + .input-group-btn %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'} %span = default_clone_protocol.upcase diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 8495774accc..c38d9313dba 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,4 +1,4 @@ -.btn-group.btn-group-next.event-filter +%ul.nav-links.event-filter = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index cbdecda4fff..f77feeb79cd 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,5 +1,5 @@ .milestones-filters - %ul.center-top-menu + %ul.nav-links %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} = link_to milestones_filter_path(state: 'opened') do Open diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index a54c5fa8c33..778b20fb4f2 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -11,7 +11,7 @@ = image_tag group_icon(group), class: "avatar s46 hidden-xs" = link_to group, class: 'group-name' do - %strong= group.name + %span.item-title= group.name - if group_member as @@ -19,4 +19,3 @@ %div.light #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")} - diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0e3e9275fc1..8d6f47b38ef 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,6 +1,6 @@ .issues-filters .issues-state-filters - %ul.center-top-menu + %ul.nav-links - if defined?(type) && type == :merge_requests - page_context_word = 'merge requests' - else diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 2299112bec7..9f4a7098ea2 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -69,15 +69,16 @@ You're not receiving notifications from this thread. .subscribed{class: ( 'hidden' unless subscribed )} You're receiving notifications because you're subscribed to this thread. + - project_ref = cross_project_reference(@project, issuable) .block .title .cross-project-reference - %span#cross-project-reference + %span Reference: - %a{href: '#', title:project_ref} + %cite{title: project_ref} = project_ref - = clipboard_button(clipboard_target: 'span#cross-project-reference') + = clipboard_button(clipboard_text: project_ref) :javascript new Subscription("#{toggle_subscription_path(issuable)}"); diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml index 4a84348ac82..83f61ce4b07 100644 --- a/app/views/sherlock/queries/show.html.haml +++ b/app/views/sherlock/queries/show.html.haml @@ -1,7 +1,7 @@ - page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query') - header_title t('sherlock.title'), sherlock_transactions_path -%ul.center-top-menu +%ul.nav-links %li.active %a(href="#tab-general" data-toggle="tab") = t('sherlock.general') diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index 3c8ffb06648..9d4b0b2724c 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -1,7 +1,7 @@ - page_title t('sherlock.title'), t('sherlock.transaction') - header_title t('sherlock.title'), sherlock_transactions_path -%ul.center-top-menu +%ul.nav-links %li.active %a(href="#tab-general" data-toggle="tab") = t('sherlock.general') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index ce17fc7bca1..3bfd781e51d 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,6 +1,7 @@ - page_title @user.name - page_description @user.bio - header_title @user.name, user_path(@user) +- @no_container = true = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") @@ -8,6 +9,25 @@ = render 'shared/show_aside' .cover-block + .cover-controls + - if @user == current_user + = link_to profile_path, class: 'btn btn-gray' do + = icon('pencil') + - elsif current_user + %span.report-abuse + - if @user.abuse_report + %button.btn.btn-danger{ title: 'Already reported for abuse', + data: { toggle: 'tooltip', placement: 'left', container: 'body' }} + = icon('exclamation-circle') + - else + = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', + title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do + = icon('exclamation-circle') + - if current_user + + = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do + = icon('rss') + .avatar-holder = link_to avatar_icon(@user, 400), target: '_blank' do = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' @@ -47,74 +67,56 @@ = icon('map-marker') = @user.location + %ul.nav-links.center + %li.active + = link_to "#activity", 'data-toggle' => 'tab' do + Activity + - if @groups.any? + %li + = link_to "#groups", 'data-toggle' => 'tab' do + Groups + - if @contributed_projects.present? + %li + = link_to "#contributed", 'data-toggle' => 'tab' do + Contributed projects + - if @projects.present? + %li + = link_to "#personal", 'data-toggle' => 'tab' do + Personal projects - .cover-controls - - if @user == current_user - = link_to profile_path, class: 'btn btn-gray' do - = icon('pencil') - - elsif current_user - %span.report-abuse - - if @user.abuse_report - %button.btn.btn-danger{ title: 'Already reported for abuse', - data: { toggle: 'tooltip', placement: 'left', container: 'body' }} - = icon('exclamation-circle') - - else - = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray', - title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do - = icon('exclamation-circle') - - if current_user - - = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do - = icon('rss') - -.gray-content-block.second-block - .user-calendar - %h4.center.light - %i.fa.fa-spinner.fa-spin - .user-calendar-activities - +%div{ class: container_class } + .tab-content + .tab-pane.active#activity + .gray-content-block.white.second-block + %div{ class: container_class } + .user-calendar + %h4.center.light + %i.fa.fa-spinner.fa-spin + .user-calendar-activities -%ul.center-top-menu.no-top.no-bottom.bottom-border.wide - %li.active - = link_to "#activity", 'data-toggle' => 'tab' do - Activity - - if @groups.any? - %li - = link_to "#groups", 'data-toggle' => 'tab' do - Groups - - if @contributed_projects.present? - %li - = link_to "#contributed", 'data-toggle' => 'tab' do - Contributed projects - - if @projects.present? - %li - = link_to "#personal", 'data-toggle' => 'tab' do - Personal projects -.tab-content - .tab-pane.active#activity - .content_list - = spinner + .content_list + = spinner - - if @groups.any? - .tab-pane#groups - %ul.content-list - - @groups.each do |group| - = render 'shared/groups/group', group: group + - if @groups.any? + .tab-pane#groups + %ul.content-list + - @groups.each do |group| + = render 'shared/groups/group', group: group - - if @contributed_projects.present? - .tab-pane#contributed - .contributed-projects - = render 'shared/projects/list', - projects: @contributed_projects.sort_by(&:star_count).reverse, - projects_limit: 5, stars: true, avatar: true + - if @contributed_projects.present? + .tab-pane#contributed + .contributed-projects + = render 'shared/projects/list', + projects: @contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: true - - if @projects.present? - .tab-pane#personal - .personal-projects - = render 'shared/projects/list', - projects: @projects.sort_by(&:star_count).reverse, - projects_limit: 10, stars: true, avatar: true + - if @projects.present? + .tab-pane#personal + .personal-projects + = render 'shared/projects/list', + projects: @projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: true :javascript $(".user-calendar").load("#{user_calendar_path}"); diff --git a/bin/background_jobs b/bin/background_jobs index 5c85fb339e6..1f67d732949 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -27,17 +27,17 @@ restart() stop fi killall - start_sidekiq -d -L $sidekiq_logfile + start_sidekiq -d -L $sidekiq_logfile >> $sidekiq_logfile 2>&1 } start_no_deamonize() { - start_sidekiq + start_sidekiq >> $sidekiq_logfile 2>&1 } start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@" } load_ok() @@ -66,6 +66,9 @@ case "$1" in start_no_deamonize) start_no_deamonize ;; + start_foreground) + start_sidekiq + ;; restart) restart ;; @@ -5,6 +5,7 @@ app_root=$(pwd) unicorn_pidfile="$app_root/tmp/pids/unicorn.pid" unicorn_config="$app_root/config/unicorn.rb" +unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV" get_unicorn_pid() { @@ -18,7 +19,12 @@ get_unicorn_pid() start() { - bundle exec unicorn_rails -D -c $unicorn_config -E $RAILS_ENV + $unicorn_cmd -D +} + +start_foreground() +{ + $unicorn_cmd } stop() @@ -37,6 +43,9 @@ case "$1" in start) start ;; + start_foreground) + start_foreground + ;; stop) stop ;; diff --git a/config/environments/development.rb b/config/environments/development.rb index c22722c606b..257c163720a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -34,6 +34,8 @@ Rails.application.configure do config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Open sent mails in browser config.action_mailer.delivery_method = :letter_opener + # Don't make a mess when bootstrapping a development environment + config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') config.eager_load = false end diff --git a/config/routes.rb b/config/routes.rb index 3d5c70987c8..75418db8d25 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -88,6 +88,12 @@ Rails.application.routes.draw do end end + resources :sent_notifications, only: [], constraints: { id: /\h{32}/ } do + member do + get :unsubscribe + end + end + # Spam reports resources :abuse_reports, only: [:new, :create] @@ -219,7 +225,7 @@ Rails.application.routes.draw do get :test end - resources :broadcast_messages, only: [:index, :create, :destroy] + resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] resource :logs, only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] @@ -513,7 +519,7 @@ Rails.application.routes.draw do end end - WIKI_SLUG_ID = { id: /[a-zA-Z.0-9_\-\/]+/ } unless defined? WIKI_SLUG_ID + WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID scope do # Order matters to give priority to these matches @@ -604,9 +610,14 @@ Rails.application.routes.draw do member do get :status post :cancel - get :download post :retry end + + resource :artifacts, only: [] do + get :download + get :browse, path: 'browse(/*path)', format: false + get :file, path: 'file/*path', format: false + end end resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb new file mode 100644 index 00000000000..03a12323845 --- /dev/null +++ b/db/fixtures/development/14_builds.rb @@ -0,0 +1,79 @@ +class Gitlab::Seeder::Builds + BUILD_STATUSES = %w(running pending success failed canceled) + + def initialize(project) + @project = project + end + + def seed! + ci_commits.each do |ci_commit| + build = Ci::Build.new(build_attributes_for(ci_commit)) + + artifacts_cache_file(artifacts_archive_path) do |file| + build.artifacts_file = file + end + + artifacts_cache_file(artifacts_metadata_path) do |file| + build.artifacts_metadata = file + end + + begin + build.save! + print '.' + rescue ActiveRecord::RecordInvalid + print 'F' + end + end + end + + def ci_commits + commits = @project.repository.commits('master', nil, 5) + commits_sha = commits.map { |commit| commit.raw.id } + commits_sha.map do |sha| + @project.ensure_ci_commit(sha) + end + rescue + [] + end + + def build_attributes_for(ci_commit) + { name: 'test build', commands: "$ build command", + stage: 'test', stage_idx: 1, ref: 'master', + user_id: build_user, gl_project_id: @project.id, + status: build_status, commit_id: ci_commit.id, + created_at: Time.now, updated_at: Time.now } + end + + def build_user + @project.team.users.sample + end + + def build_status + BUILD_STATUSES.sample + end + + def artifacts_archive_path + Rails.root + 'spec/fixtures/ci_build_artifacts.zip' + end + + def artifacts_metadata_path + Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + + end + + def artifacts_cache_file(file_path) + cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_") + + FileUtils.copy(file_path, cache_path) + File.open(cache_path) do |file| + yield file + end + end +end + +Gitlab::Seeder.quiet do + Project.all.sample(10).each do |project| + project_builds = Gitlab::Seeder::Builds.new(project) + project_builds.seed! + end +end diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb new file mode 100644 index 00000000000..6c282fc5039 --- /dev/null +++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb @@ -0,0 +1,5 @@ +class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration + def change + add_column :ci_builds, :artifacts_metadata, :text + end +end diff --git a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb new file mode 100644 index 00000000000..78fdfeaf5cf --- /dev/null +++ b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb @@ -0,0 +1,5 @@ +class RemoveAlertTypeFromBroadcastMessages < ActiveRecord::Migration + def change + remove_column :broadcast_messages, :alert_type, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index ecbe575bf83..2fc8c4d5ed4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -82,7 +82,6 @@ ActiveRecord::Schema.define(version: 20160113111034) do t.text "message", null: false t.datetime "starts_at" t.datetime "ends_at" - t.integer "alert_type" t.datetime "created_at" t.datetime "updated_at" t.string "color" @@ -124,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160113111034) do t.string "description" t.text "artifacts_file" t.integer "gl_project_id" + t.text "artifacts_metadata" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree diff --git a/doc/api/README.md b/doc/api/README.md index 25a31b235cc..2fa177ff4dd 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -23,6 +23,9 @@ - [Namespaces](namespaces.md) - [Settings](settings.md) - [Keys](keys.md) +- [Builds](builds.md) +- [Build triggers](build_triggers.md) +- [Build Variables](build_variables.md) ## Clients diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md new file mode 100644 index 00000000000..4a12e962b62 --- /dev/null +++ b/doc/api/build_triggers.md @@ -0,0 +1,108 @@ +# Build triggers + +You can read more about [triggering builds through the API](../ci/triggers/README.md). + +## List project triggers + +Get a list of project's build triggers. + +``` +GET /projects/:id/triggers +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +``` + +```json +[ + { + "created_at": "2015-12-23T16:24:34.716Z", + "deleted_at": null, + "last_used": "2016-01-04T15:41:21.986Z", + "token": "fbdb730c2fbdb095a0862dbd8ab88b", + "updated_at": "2015-12-23T16:24:34.716Z" + }, + { + "created_at": "2015-12-23T16:25:56.760Z", + "deleted_at": null, + "last_used": null, + "token": "7b9148c158980bbd9bcea92c17522d", + "updated_at": "2015-12-23T16:25:56.760Z" + } +] +``` + +## Get trigger details + +Get details of project's build trigger. + +``` +GET /projects/:id/triggers/:token +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `token` | string | yes | The `token` of a trigger | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +``` + +```json +{ + "created_at": "2015-12-23T16:25:56.760Z", + "deleted_at": null, + "last_used": null, + "token": "7b9148c158980bbd9bcea92c17522d", + "updated_at": "2015-12-23T16:25:56.760Z" +} +``` + +## Create a project trigger + +Create a build trigger for a project. + +``` +POST /projects/:id/triggers +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +``` + +```json +{ + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z" +} +``` + +## Remove a project trigger + +Remove a project's build trigger. + +``` +DELETE /projects/:id/triggers/:token +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `token` | string | yes | The `token` of a project | + +``` +curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +``` diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md new file mode 100644 index 00000000000..b96f1bdac8a --- /dev/null +++ b/doc/api/build_variables.md @@ -0,0 +1,128 @@ +# Build Variables + +## List project variables + +Get list of a project's build variables. + +``` +GET /projects/:id/variables +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" +``` + +```json +[ + { + "key": "TEST_VARIABLE_1", + "value": "TEST_1" + }, + { + "key": "TEST_VARIABLE_2", + "value": "TEST_2" + } +] +``` + +## Show variable details + +Get the details of a project's specific build variable. + +``` +GET /projects/:id/variables/:key +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-----------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" +``` + +```json +{ + "key": "TEST_VARIABLE_1", + "value": "TEST_1" +} +``` + +## Create variable + +Create a new build variable. + +``` +POST /projects/:id/variables +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-----------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed | +| `value` | string | yes | The `value` of a variable | + +``` +curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value" +``` + +```json +{ + "key": "NEW_VARIABLE", + "value": "new value" +} +``` + +## Update variable + +Update a project's build variable. + +``` +PUT /projects/:id/variables/:key +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-------------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable | +| `value` | string | yes | The `value` of a variable | + +``` +curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value" +``` + +```json +{ + "key": "NEW_VARIABLE", + "value": "updated value" +} +``` + +## Remove variable + +Remove a project's build variable. + +``` +DELETE /projects/:id/variables/:key +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-------------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable | + +``` +curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" +``` + +```json +{ + "key": "VARIABLE_1", + "value": "VALUE_1" +} +``` diff --git a/doc/api/builds.md b/doc/api/builds.md new file mode 100644 index 00000000000..ecb50754c88 --- /dev/null +++ b/doc/api/builds.md @@ -0,0 +1,360 @@ +# Builds API + +## List project builds + +Get a list of builds in a project. + +``` +GET /projects/:id/builds +``` + +### Parameters + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | + +### Example of request + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" +``` + +### Example of response + +```json +[ + { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2015-12-24T15:51:21.802Z", + "download_url": null, + "finished_at": "2015-12-24T17:54:27.895Z", + "id": 7, + "name": "teaspoon", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": "2015-12-24T17:54:27.722Z", + "status": "failed", + "tag": false, + "user": { + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "bio": null, + "created_at": "2015-12-21T13:14:24.077Z", + "id": 1, + "is_admin": true, + "linkedin": "", + "name": "Administrator", + "skype": "", + "state": "active", + "twitter": "", + "username": "root", + "web_url": "http://gitlab.dev/u/root", + "website_url": "" + } + }, + { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2015-12-24T15:51:21.727Z", + "download_url": null, + "finished_at": "2015-12-24T17:54:24.921Z", + "id": 6, + "name": "spinach:other", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": "2015-12-24T17:54:24.729Z", + "status": "failed", + "tag": false, + "user": { + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "bio": null, + "created_at": "2015-12-21T13:14:24.077Z", + "id": 1, + "is_admin": true, + "linkedin": "", + "name": "Administrator", + "skype": "", + "state": "active", + "twitter": "", + "username": "root", + "web_url": "http://gitlab.dev/u/root", + "website_url": "" + } + } +] +``` + +## List commit builds + +Get a list of builds for specific commit in a project. + +``` +GET /projects/:id/repository/commits/:sha/builds +``` + +### Parameters + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| sha | string | yes | The SHA id of a commit | +| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | + +### Example of request + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" +``` + +### Example of response + +```json +[ + { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2016-01-11T10:13:33.506Z", + "download_url": null, + "finished_at": "2016-01-11T10:14:09.526Z", + "id": 69, + "name": "rubocop", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": null, + "status": "canceled", + "tag": false, + "user": null + }, + { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2015-12-24T15:51:21.957Z", + "download_url": null, + "finished_at": "2015-12-24T17:54:33.913Z", + "id": 9, + "name": "brakeman", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": "2015-12-24T17:54:33.727Z", + "status": "failed", + "tag": false, + "user": { + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "bio": null, + "created_at": "2015-12-21T13:14:24.077Z", + "id": 1, + "is_admin": true, + "linkedin": "", + "name": "Administrator", + "skype": "", + "state": "active", + "twitter": "", + "username": "root", + "web_url": "http://gitlab.dev/u/root", + "website_url": "" + } + } +] +``` + +## Get a single build + +Get a single build of a project + +``` +GET /projects/:id/builds/:build_id +``` + +### Parameters + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| build\_id | integer | yes | The ID of a build | + +### Example of request + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" +``` + +### Example of response + +```json +{ + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2015-12-24T15:51:21.880Z", + "download_url": null, + "finished_at": "2015-12-24T17:54:31.198Z", + "id": 8, + "name": "rubocop", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": "2015-12-24T17:54:30.733Z", + "status": "failed", + "tag": false, + "user": { + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "bio": null, + "created_at": "2015-12-21T13:14:24.077Z", + "id": 1, + "is_admin": true, + "linkedin": "", + "name": "Administrator", + "skype": "", + "state": "active", + "twitter": "", + "username": "root", + "web_url": "http://gitlab.dev/u/root", + "website_url": "" + } +} +``` + +## Cancel a build + +Cancel a single build of a project + +``` +POST /projects/:id/builds/:build_id/cancel +``` + +### Parameters + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| build\_id | integer | yes | The ID of a build | + +### Example of request + +``` +curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" +``` + +### Example of response + +```json +{ + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2016-01-11T10:13:33.506Z", + "download_url": null, + "finished_at": "2016-01-11T10:14:09.526Z", + "id": 69, + "name": "rubocop", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": null, + "status": "canceled", + "tag": false, + "user": null +} +``` + +## Retry a build + +Retry a single build of a project + +``` +POST /projects/:id/builds/:build_id/retry +``` + +### Parameters + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| build\_id | integer | yes | The ID of a build | + +### Example of request + +``` +curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" +``` + +### Example of response + +```json +{ + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2016-01-11T10:13:33.506Z", + "download_url": null, + "finished_at": null, + "id": 69, + "name": "rubocop", + "ref": "master", + "runner": null, + "stage": "test", + "started_at": null, + "status": "pending", + "tag": false, + "user": null +} +``` diff --git a/doc/api/users.md b/doc/api/users.md index 773fe36d277..b7fc903825e 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -558,7 +558,8 @@ Parameters: - `uid` (required) - id of specified user -Will return `200 OK` on success, or `404 User Not Found` is user cannot be found. +Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +`403 Forbidden` when trying to block an already blocked user by LDAP synchronization. ## Unblock user @@ -572,4 +573,5 @@ Parameters: - `uid` (required) - id of specified user -Will return `200 OK` on success, or `404 User Not Found` is user cannot be found. +Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md index 3b5008ccdb4..018ca22dbbd 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -18,18 +18,62 @@ Returns: ```json { - "id" : 79, - "commands" : "", - "path" : "", - "ref" : "", - "sha" : "", - "project_id" : 6, - "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "before_sha" : "" + "id": 48584, + "ref": "0.1.1", + "tag": true, + "sha": "d63117656af6ff57d99e50cc270f854691f335ad", + "status": "success", + "name": "pages", + "token": "9dd60b4f1a439d1765357446c1084c", + "stage": "test", + "project_id": 479, + "project_name": "test", + "commands": "echo commands", + "repo_url": "http://gitlab-ci-token:token@gitlab.example/group/test.git", + "before_sha": "0000000000000000000000000000000000000000", + "allow_git_fetch": false, + "options": { + "image": "docker:image", + "artifacts": { + "paths": [ + "public" + ] + }, + "cache": { + "paths": [ + "vendor" + ] + } + }, + "timeout": 3600, + "variables": [ + { + "key": "CI_BUILD_TAG", + "value": "0.1.1", + "public": true + } + ], + "depends_on_builds": [ + { + "id": 48584, + "ref": "0.1.1", + "tag": true, + "sha": "d63117656af6ff57d99e50cc270f854691f335ad", + "status": "success", + "name": "build", + "token": "9dd60b4f1a439d1765357446c1084c", + "stage": "build", + "project_id": 479, + "project_name": "test", + "artifacts_file": { + "filename": "artifacts.zip", + "size": 0 + } + } + ] } ``` - ### Update details of an existing build PUT /ci/builds/:id diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 86d205ba7a5..4cfb8402943 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -74,10 +74,11 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. -1. Reconfigure GitLab for the changes to take effect: +1. Reconfigure GitLab and restart mailroom for the changes to take effect: ```sh sudo gitlab-ctl reconfigure + sudo gitlab-ctl restart mailroom ``` 1. Verify that everything is configured correctly: diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md index 18bf3db1744..787d21f7f8f 100644 --- a/doc/incoming_email/postfix.md +++ b/doc/incoming_email/postfix.md @@ -84,7 +84,12 @@ The instructions make the assumption that you will be using the email address `i quit ``` - (Note: The `.` is a literal period on its own line) + _**Note:** The `.` is a literal period on its own line._ + + _**Note:** If you receive an error after entering `rcpt to: incoming@localhost` + then your Postfix `my_network` configuration is not correct. The error will + say 'Temporary lookup failure'. See + [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._ 1. Check if the `incoming` user received the email: @@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo 1. Test the new setup: 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_. - 2. Check if the `incoming` user received the email: + 1. Check if the `incoming` user received the email: ```sh su - incoming @@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo q ``` + _**Note:** If `mail` returns an error `Maildir: Is a directory` then your + version of `mail` doesn't support Maildir style mailboxes. Install + `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then, + try the above steps again, substituting `heirloom-mailx` for the `mail` + command._ + 1. Log out of the `incoming` account and go back to being `root`: ```sh diff --git a/doc/install/installation.md b/doc/install/installation.md index e645445df2a..00030729a4b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -135,11 +135,11 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). - curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz - echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \ - sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz + curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz + echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -c - && \ + sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.5.1.linux-amd64.tar.gz + rm go1.5.3.linux-amd64.tar.gz ## 4. System Users diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 1632e42f701..8841dbdb7c6 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -78,6 +78,18 @@ On the sign in page there should now be a SAML button below the regular sign in ## Troubleshooting -If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user. +If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, +this likely indicates that GitLab could not get the email address for the SAML user. -Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username.
\ No newline at end of file +Make sure the IdP provides a claim containing the user's email address, using claim name +'email' or 'mail'. The email will be used to automatically generate the GitLab username. + +If after signing in into your SAML server you are redirected back to the sign in page and +no error is displayed, check your `production.log` file. It will most likely contain the +message `Can't verify CSRF token authenticity`. This means that there is an error during +the SAML request, but this error never reaches GitLab due to the CSRF check. + +To bypass this you can add `skip_before_action :verify_authenticity_token` to the +`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, +where it can then be seen in the usual logs, or as a flash message in the login +screen.
\ No newline at end of file diff --git a/doc/workflow/importing/github_importer/importer.png b/doc/workflow/importing/github_importer/importer.png Binary files differdeleted file mode 100644 index 57636717571..00000000000 --- a/doc/workflow/importing/github_importer/importer.png +++ /dev/null diff --git a/doc/workflow/importing/github_importer/new_project_page.png b/doc/workflow/importing/github_importer/new_project_page.png Binary files differdeleted file mode 100644 index 002f22d81d7..00000000000 --- a/doc/workflow/importing/github_importer/new_project_page.png +++ /dev/null diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png Binary files differnew file mode 100644 index 00000000000..f744dc06f81 --- /dev/null +++ b/doc/workflow/importing/img/import_projects_from_github_importer.png diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png Binary files differnew file mode 100644 index 00000000000..86be35acb37 --- /dev/null +++ b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 2027a055c37..f693f430a42 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -1,20 +1,44 @@ # Import your project from GitHub to GitLab
-It takes just a couple of steps to import your existing GitHub projects to GitLab. Keep in mind that it is possible only if
-GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html)
+_**Note:** In order to enable the GitHub import setting, you should first
+enable the [GitHub integration][gh-import] in your GitLab instance._
-If you want to import from a GitHub Enterprise instance, you need to use GitLab Enterprise; please see the [EE docs for the GitHub integration](http://doc.gitlab.com/ee/integration/github.html).
+At its current state, GitHub importer can import:
-* Sign in to GitLab.com and go to your dashboard.
-* To get to the importer page, you need to go to the "New project" page.
+- the repository description (introduced in GitLab 7.7)
+- the git repository data (introduced in GitLab 7.7)
+- the issues (introduced in GitLab 7.7)
+- the pull requests (introduced in GitLab 8.4)
+- the wiki pages (introduced in GitLab 8.4)
-![New project page](github_importer/new_project_page.png)
+It is not yet possible to import your labels, milestones and cross-repository
+pull requests (those from forks). We are working on improving this in the near
+future.
-* Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
+The importer page is visible when you [create a new project][new-project].
+Click on the **GitHub** link and you will be redirected to GitHub for
+permission to access your projects. After accepting, you'll be automatically
+redirected to the importer.
-![Importer page](github_importer/importer.png)
+![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
-* To import a project, you can simple click "Add". The importer will import your repository, issues, and pull requests. Once the importer is done, a new GitLab project will be created with your imported data.
+---
-### Note
-When you import your projects from GitHub, it is not possible to keep your labels, milestones, and cross-repository pull requests. We are working on improving this in the near future.
+While at the GitHub importer page, you can see the import statuses of your
+GitHub projects. Those that are being imported will show a _started_ status,
+those already imported will be green, whereas those that are not yet imported
+have an **Import** button on the right side of the table. If you want, you can
+import all your GitHub projects in one go by hitting **Import all projects**
+in the upper left corner.
+
+![GitHub importer page](img/import_projects_from_github_importer.png)
+
+---
+
+The importer will create any new namespaces if they don't exist or in the
+case the namespace is taken, the project will be imported on the user's
+namespace.
+
+[gh-import]: ../../integration/github.md "GitHub integration"
+[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
+[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature index b2c3112320a..fd3bac77f86 100644 --- a/features/admin/broadcast_messages.feature +++ b/features/admin/broadcast_messages.feature @@ -2,16 +2,11 @@ Feature: Admin Broadcast Messages Background: Given I sign in as an admin - And application already has admin messages + And application already has a broadcast message And I visit admin messages page Scenario: See broadcast messages list - Then I should be all broadcast messages - - Scenario: Create a broadcast message - When submit form with new broadcast message - Then I should be redirected to admin messages page - And I should see newly created broadcast message + Then I should see all broadcast messages Scenario: Create a customized broadcast message When submit form with new customized broadcast message @@ -19,3 +14,14 @@ Feature: Admin Broadcast Messages And I should see newly created broadcast message Then I visit dashboard page And I should see a customized broadcast message + + Scenario: Edit an existing broadcast message + When I edit an existing broadcast message + And I change the broadcast message text + Then I should be redirected to admin messages page + And I should see the updated broadcast message + + Scenario: Remove an existing broadcast message + When I remove an existing broadcast message + Then I should be redirected to admin messages page + And I should not see the removed broadcast message diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature new file mode 100644 index 00000000000..7a7dbb71b18 --- /dev/null +++ b/features/project/builds/artifacts.feature @@ -0,0 +1,53 @@ +Feature: Project Builds Artifacts + Background: + Given I sign in as a user + And I own a project + And project has CI enabled + And project has a recent build + + Scenario: I download build artifacts + Given recent build has artifacts available + When I visit recent build summary page + And I click artifacts download button + Then download of build artifacts archive starts + + Scenario: I browse build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build summary page + And I click artifacts browse button + Then I should see content of artifacts archive + + Scenario: I browse subdirectory of build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build summary page + And I click artifacts browse button + And I click link to subdirectory within build artifacts + Then I should see content of subdirectory within artifacts archive + + Scenario: I browse directory with UTF-8 characters in name + Given recent build has artifacts available + And recent build has artifacts metadata available + And recent build artifacts contain directory with UTF-8 characters + When I visit recent build summary page + And I click artifacts browse button + And I navigate to directory with UTF-8 characters in name + Then I should see content of directory with UTF-8 characters in name + + Scenario: I try to browse directory with invalid UTF-8 characters in name + Given recent build has artifacts available + And recent build has artifacts metadata available + And recent build artifacts contain directory with invalid UTF-8 characters + When I visit recent build summary page + And I click artifacts browse button + And I navigate to parent directory of directory with invalid name + Then I should not see directory with invalid name on the list + + Scenario: I download a single file from build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build summary page + And I click artifacts browse button + And I click download button for a file within build artifacts + Then download of a file extracted from build artifacts should start diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature new file mode 100644 index 00000000000..1193bcd74f6 --- /dev/null +++ b/features/project/builds/permissions.feature @@ -0,0 +1,18 @@ +Feature: Project Builds Permissions + Background: + Given I sign in as a user + And project exists in some group namespace + And project has CI enabled + And project has a recent build + + Scenario: I try to download build artifacts as guest + Given I am member of a project with a guest role + And recent build has artifacts available + When I access artifacts download page + Then page status code should be 404 + + Scenario: I try to download build artifacts as reporter + Given I am member of a project with a reporter role + And recent build has artifacts available + When I access artifacts download page + Then download of build artifacts archive starts diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature new file mode 100644 index 00000000000..e90ea592aab --- /dev/null +++ b/features/project/builds/summary.feature @@ -0,0 +1,11 @@ +Feature: Project Builds Summary + Background: + Given I sign in as a user + And I own a project + And project has CI enabled + And project has a recent build + + Scenario: I browse build summary page + When I visit recent build summary page + Then I see summary for build + And I see build trace diff --git a/features/project/issues/references.feature b/features/project/issues/references.feature new file mode 100644 index 00000000000..4ae2d653337 --- /dev/null +++ b/features/project/issues/references.feature @@ -0,0 +1,33 @@ +@project_issues +Feature: Project Issues References + Background: + Given I sign in as "John Doe" + And public project "Community" + And "John Doe" owns public project "Community" + And project "Community" has "Community issue" open issue + And I logout + And I sign in as "Mary Jane" + And private project "Enterprise" + And "Mary Jane" owns private project "Enterprise" + And project "Enterprise" has "Enterprise issue" open issue + And project "Enterprise" has "Enterprise fix" open merge request + And I visit issue page "Enterprise issue" + And I leave a comment referencing issue "Community issue" + And I visit merge request page "Enterprise fix" + And I leave a comment referencing issue "Community issue" + And I logout + + @javascript + Scenario: Viewing the public issue as a "John Doe" + Given I sign in as "John Doe" + When I visit issue page "Community issue" + Then I should not see any related merge requests + And I should see no notes at all + + @javascript + Scenario: Viewing the public issue as "Mary Jane" + Given I sign in as "Mary Jane" + When I visit issue page "Community issue" + Then I should see the "Enterprise fix" related merge request + And I should see a note linking to "Enterprise fix" merge request + And I should see a note linking to "Enterprise issue" issue diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature new file mode 100644 index 00000000000..571612261a9 --- /dev/null +++ b/features/project/merge_requests/references.feature @@ -0,0 +1,31 @@ +@project_merge_requests +Feature: Project Merge Requests References + Background: + Given I sign in as "John Doe" + And public project "Community" + And "John Doe" owns public project "Community" + And project "Community" has "Community fix" open merge request + And I logout + And I sign in as "Mary Jane" + And private project "Enterprise" + And "Mary Jane" owns private project "Enterprise" + And project "Enterprise" has "Enterprise issue" open issue + And project "Enterprise" has "Enterprise fix" open merge request + And I visit issue page "Enterprise issue" + And I leave a comment referencing issue "Community fix" + And I visit merge request page "Enterprise fix" + And I leave a comment referencing issue "Community fix" + And I logout + + @javascript + Scenario: Viewing the public issue as a "John Doe" + Given I sign in as "John Doe" + When I visit issue page "Community fix" + Then I should see no notes at all + + @javascript + Scenario: Viewing the public issue as "Mary Jane" + Given I sign in as "Mary Jane" + When I visit issue page "Community fix" + And I should see a note linking to "Enterprise fix" merge request + And I should see a note linking to "Enterprise issue" issue diff --git a/features/project/wiki.feature b/features/project/wiki.feature index af970ecf2d0..d4811b1ff54 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -70,11 +70,6 @@ Feature: Project Wiki Then I should see non-escaped link in the pages list @javascript - Scenario: Creating an invalid new page - Given I create a New page with an invalid name - Then I should see an error message - - @javascript Scenario: Edit Wiki page that has a path Given I create a New page with paths And I click on the "Pages" button diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb index f6daf852977..6cacdf4764c 100644 --- a/features/steps/admin/broadcast_messages.rb +++ b/features/steps/admin/broadcast_messages.rb @@ -1,22 +1,15 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps include SharedAuthentication include SharedPaths - include SharedAdmin - step 'application already has admin messages' do - FactoryGirl.create(:broadcast_message, message: "Migration to new server") + step 'application already has a broadcast message' do + FactoryGirl.create(:broadcast_message, :expired, message: "Migration to new server") end - step 'I should be all broadcast messages' do + step 'I should see all broadcast messages' do expect(page).to have_content "Migration to new server" end - step 'submit form with new broadcast message' do - fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST' - select '2018', from: "broadcast_message_ends_at_1i" - click_button "Add broadcast message" - end - step 'I should be redirected to admin messages page' do expect(current_path).to eq admin_broadcast_messages_path end @@ -27,10 +20,9 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps step 'submit form with new customized broadcast message' do fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST' - click_link "Customize colors" fill_in 'broadcast_message_color', with: '#f2dede' fill_in 'broadcast_message_font', with: '#b94a48' - select '2018', from: "broadcast_message_ends_at_1i" + select Date.today.next_year.year, from: "broadcast_message_ends_at_1i" click_button "Add broadcast message" end @@ -38,4 +30,25 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) end + + step 'I edit an existing broadcast message' do + click_link 'Edit' + end + + step 'I change the broadcast message text' do + fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW' + click_button 'Update broadcast message' + end + + step 'I should see the updated broadcast message' do + expect(page).to have_content "Application update RIGHT NOW" + end + + step 'I remove an existing broadcast message' do + click_link 'Remove' + end + + step 'I should not see the removed broadcast message' do + expect(page).not_to have_content 'Migration to new server' + end end diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb new file mode 100644 index 00000000000..f2c87da4717 --- /dev/null +++ b/features/steps/project/builds/artifacts.rb @@ -0,0 +1,76 @@ +class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include RepoHelpers + + step 'I click artifacts download button' do + page.within('.artifacts') { click_link 'Download' } + end + + step 'I click artifacts browse button' do + page.within('.artifacts') { click_link 'Browse' } + end + + step 'I should see content of artifacts archive' do + page.within('.tree-table') do + expect(page).to have_no_content '..' + expect(page).to have_content 'other_artifacts_0.1.2' + expect(page).to have_content 'ci_artifacts.txt' + expect(page).to have_content 'rails_sample.jpg' + end + end + + step 'I click link to subdirectory within build artifacts' do + page.within('.tree-table') { click_link 'other_artifacts_0.1.2' } + end + + step 'I should see content of subdirectory within artifacts archive' do + page.within('.tree-table') do + expect(page).to have_content '..' + expect(page).to have_content 'another-subdirectory' + expect(page).to have_content 'doc_sample.txt' + end + end + + step 'recent build artifacts contain directory with UTF-8 characters' do + # metadata fixture contains relevant directory + end + + step 'I navigate to directory with UTF-8 characters in name' do + page.within('.tree-table') { click_link 'tests_encoding' } + page.within('.tree-table') { click_link 'utf8 test dir ✓' } + end + + step 'I should see content of directory with UTF-8 characters in name' do + page.within('.tree-table') do + expect(page).to have_content '..' + expect(page).to have_content 'regular_file_2' + end + end + + step 'recent build artifacts contain directory with invalid UTF-8 characters' do + # metadata fixture contains relevant directory + end + + step 'I navigate to parent directory of directory with invalid name' do + page.within('.tree-table') { click_link 'tests_encoding' } + end + + step 'I should not see directory with invalid name on the list' do + page.within('.tree-table') do + expect(page).to have_no_content('non-utf8-dir') + end + end + + step 'I click download button for a file within build artifacts' do + page.within('.tree-table') { first('.artifact-download').click } + end + + step 'download of a file extracted from build artifacts should start' do + # this will be accelerated by Workhorse + response_json = JSON.parse(page.body, symbolize_names: true) + expect(response_json[:archive]).to end_with('build_artifacts.zip') + expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt') + end +end diff --git a/features/steps/project/builds/permissions.rb b/features/steps/project/builds/permissions.rb new file mode 100644 index 00000000000..6e9d6504fd5 --- /dev/null +++ b/features/steps/project/builds/permissions.rb @@ -0,0 +1,7 @@ +class Spinach::Features::ProjectBuildsPermissions < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include SharedPaths + include RepoHelpers +end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb new file mode 100644 index 00000000000..2439d48fbef --- /dev/null +++ b/features/steps/project/builds/summary.rb @@ -0,0 +1,14 @@ +class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include RepoHelpers + + step 'I see summary for build' do + expect(page).to have_content "Build ##{@build.id}" + end + + step 'I see build trace' do + expect(page).to have_css '#build-trace' + end +end diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb new file mode 100644 index 00000000000..69e8b5cbde5 --- /dev/null +++ b/features/steps/project/issues/references.rb @@ -0,0 +1,7 @@ +class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedNote + include SharedProject + include SharedUser +end diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb new file mode 100644 index 00000000000..ab2ae6847a2 --- /dev/null +++ b/features/steps/project/merge_requests/references.rb @@ -0,0 +1,7 @@ +class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedNote + include SharedProject + include SharedUser +end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 91d227fadbf..d753ae14590 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -132,16 +132,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps expect(current_path).to include 'one/two/three' end - step 'I create a New page with an invalid name' do - click_on 'New Page' - fill_in 'Page slug', with: 'invalid name' - click_on 'Create Page' - end - - step 'I should see an error message' do - expect(page).to have_content "The page slug is invalid" - end - step 'I should see non-escaped link in the pages list' do expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index eb2ccd9d01e..0bee91d758d 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -6,7 +6,7 @@ module SharedActiveTab end def ensure_active_sub_tab(content) - expect(find('div.content ul.center-top-menu li.active')).to have_content(content) + expect(find('div.content ul.nav-links li.active')).to have_content(content) end def ensure_active_sub_nav(content) @@ -18,7 +18,7 @@ module SharedActiveTab end step 'no other sub tabs should be active' do - expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1) + expect(page).to have_selector('div.content ul.nav-links li.active', count: 1) end step 'no other sub navs should be active' do diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb new file mode 100644 index 00000000000..f88b01af84e --- /dev/null +++ b/features/steps/shared/builds.rb @@ -0,0 +1,37 @@ +module SharedBuilds + include Spinach::DSL + + step 'project has CI enabled' do + @project.enable_ci + end + + step 'project has a recent build' do + ci_commit = create :ci_commit, project: @project, sha: sample_commit.id + @build = create :ci_build, commit: ci_commit + end + + step 'I visit recent build summary page' do + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + step 'recent build has artifacts available' do + artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip' + archive = fixture_file_upload(artifacts, 'application/zip') + @build.update_attributes(artifacts_file: archive) + end + + step 'recent build has artifacts metadata available' do + metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + gzip = fixture_file_upload(metadata, 'application/x-gzip') + @build.update_attributes(artifacts_metadata: gzip) + end + + step 'download of build artifacts archive starts' do + expect(page.response_headers['Content-Type']).to eq 'application/zip' + expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary' + end + + step 'I access artifacts download page' do + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build) + end +end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index e6d1b8b8efc..4c5f7488efb 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -5,6 +5,99 @@ module SharedIssuable find(:css, '.issuable-edit').click end + step 'project "Community" has "Community issue" open issue' do + create_issuable_for_project( + project_name: 'Community', + title: 'Community issue' + ) + end + + step 'project "Community" has "Community fix" open merge request' do + create_issuable_for_project( + project_name: 'Community', + type: :merge_request, + title: 'Community fix' + ) + end + + step 'project "Enterprise" has "Enterprise issue" open issue' do + create_issuable_for_project( + project_name: 'Enterprise', + title: 'Enterprise issue' + ) + end + + step 'project "Enterprise" has "Enterprise fix" open merge request' do + create_issuable_for_project( + project_name: 'Enterprise', + type: :merge_request, + title: 'Enterprise fix' + ) + end + + step 'I leave a comment referencing issue "Community issue"' do + leave_reference_comment( + issuable: Issue.find_by(title: 'Community issue'), + from_project_name: 'Enterprise' + ) + end + + step 'I leave a comment referencing issue "Community fix"' do + leave_reference_comment( + issuable: MergeRequest.find_by(title: 'Community fix'), + from_project_name: 'Enterprise' + ) + end + + step 'I visit issue page "Enterprise issue"' do + issue = Issue.find_by(title: 'Enterprise issue') + visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) + end + + step 'I visit merge request page "Enterprise fix"' do + mr = MergeRequest.find_by(title: 'Enterprise fix') + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) + end + + step 'I visit issue page "Community issue"' do + issue = Issue.find_by(title: 'Community issue') + visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) + end + + step 'I visit issue page "Community fix"' do + mr = MergeRequest.find_by(title: 'Community fix') + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) + end + + step 'I should not see any related merge requests' do + page.within '.issue-details' do + expect(page).not_to have_content('.merge-requests') + end + end + + step 'I should see the "Enterprise fix" related merge request' do + page.within '.merge-requests' do + expect(page).to have_content('1 Related Merge Request') + expect(page).to have_content('Enterprise fix') + end + end + + step 'I should see a note linking to "Enterprise fix" merge request' do + visible_note( + issuable: MergeRequest.find_by(title: 'Enterprise fix'), + from_project_name: 'Community', + user_name: 'Mary Jane' + ) + end + + step 'I should see a note linking to "Enterprise issue" issue' do + visible_note( + issuable: Issue.find_by(title: 'Enterprise issue'), + from_project_name: 'Community', + user_name: 'Mary Jane' + ) + end + step 'I click link "Edit" for the merge request' do edit_issuable end @@ -12,4 +105,45 @@ module SharedIssuable step 'I click link "Edit" for the issue' do edit_issuable end + + def create_issuable_for_project(project_name:, title:, type: :issue) + project = Project.find_by(name: project_name) + + attrs = { + title: title, + author: project.users.first, + description: '# Description header' + } + + case type + when :issue + attrs.merge!(project: project) + when :merge_request + attrs.merge!( + source_project: project, + target_project: project, + source_branch: 'fix', + target_branch: 'master' + ) + end + + create(type, attrs) + end + + def leave_reference_comment(issuable:, from_project_name:) + project = Project.find_by(name: from_project_name) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "##{issuable.to_reference(project)}" + click_button 'Add Comment' + end + end + + def visible_note(issuable:, from_project_name:, user_name:) + project = Project.find_by(name: from_project_name) + + expect(page).to have_content(user_name) + expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") + end + end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index f6aabfefeff..444d6726f99 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -106,6 +106,10 @@ module SharedNote end end + step 'I should see no notes at all' do + expect(page).to_not have_css('.note') + end + # Markdown step 'I leave a comment with a header containing "Comment with a header"' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index da643bf3ba9..d9c75d12238 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -7,6 +7,11 @@ module SharedProject @project.team << [@user, :master] end + step "project exists in some group namespace" do + @group = create(:group, name: 'some group') + @project = create(:project, namespace: @group) + end + # Create a specific project called "Shop" step 'I own project "Shop"' do @project = Project.find_by(name: "Shop") @@ -98,6 +103,18 @@ module SharedProject end # ---------------------------------------- + # Project permissions + # ---------------------------------------- + + step 'I am member of a project with a guest role' do + @project.team << [@user, Gitlab::Access::GUEST] + end + + step 'I am member of a project with a reporter role' do + @project.team << [@user, Gitlab::Access::REPORTER] + end + + # ---------------------------------------- # Visibility of archived project # ---------------------------------------- @@ -161,24 +178,33 @@ module SharedProject end step '"John Doe" owns private project "Enterprise"' do - user = user_exists("John Doe", username: "john_doe") - project = Project.find_by(name: "Enterprise") - project ||= create(:empty_project, name: "Enterprise", namespace: user.namespace) - project.team << [user, :master] + user_owns_project( + user_name: 'John Doe', + project_name: 'Enterprise' + ) + end + + step '"Mary Jane" owns private project "Enterprise"' do + user_owns_project( + user_name: 'Mary Jane', + project_name: 'Enterprise' + ) end step '"John Doe" owns internal project "Internal"' do - user = user_exists("John Doe", username: "john_doe") - project = Project.find_by(name: "Internal") - project ||= create :empty_project, :internal, name: 'Internal', namespace: user.namespace - project.team << [user, :master] + user_owns_project( + user_name: 'John Doe', + project_name: 'Internal', + visibility: :internal + ) end step '"John Doe" owns public project "Community"' do - user = user_exists("John Doe", username: "john_doe") - project = Project.find_by(name: "Community") - project ||= create :empty_project, :public, name: 'Community', namespace: user.namespace - project.team << [user, :master] + user_owns_project( + user_name: 'John Doe', + project_name: 'Community', + visibility: :public + ) end step 'public empty project "Empty Public Project"' do @@ -213,4 +239,11 @@ module SharedProject expect(page).to have_content("skipped") end end + + def user_owns_project(user_name:, project_name:, visibility: :private) + user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore) + project = Project.find_by(name: project_name) + project ||= create(:empty_project, visibility, name: project_name, namespace: user.namespace) + project.team << [user, :master] + end end diff --git a/lib/api/api.rb b/lib/api/api.rb index 7834262d612..7efe0a0262f 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -54,5 +54,7 @@ module API mount Keys mount Tags mount Triggers + mount Builds + mount Variables end end diff --git a/lib/api/builds.rb b/lib/api/builds.rb new file mode 100644 index 00000000000..d293f988165 --- /dev/null +++ b/lib/api/builds.rb @@ -0,0 +1,149 @@ +module API + # Projects builds API + class Builds < Grape::API + before { authenticate! } + + resource :projects do + # Get a project builds + # + # Parameters: + # id (required) - The ID of a project + # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled; + # if none provided showing all builds) + # Example Request: + # GET /projects/:id/builds + get ':id/builds' do + builds = user_project.builds.order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + + # Get builds for a specific commit of a project + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The SHA id of a commit + # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled; + # if none provided showing all builds) + # Example Request: + # GET /projects/:id/repository/commits/:sha/builds + get ':id/repository/commits/:sha/builds' do + commit = user_project.ci_commits.find_by_sha(params[:sha]) + return not_found! unless commit + + builds = commit.builds.order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + + # Get a specific build of a project + # + # Parameters: + # id (required) - The ID of a project + # build_id (required) - The ID of a build + # Example Request: + # GET /projects/:id/builds/:build_id + get ':id/builds/:build_id' do + build = get_build(params[:build_id]) + return not_found!(build) unless build + + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + + # Get a trace of a specific build of a project + # + # Parameters: + # id (required) - The ID of a project + # build_id (required) - The ID of a build + # Example Request: + # GET /projects/:id/build/:build_id/trace + # + # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace + # is saved in the DB instead of file). But before that, we need to consider how to replace the value of + # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. + get ':id/builds/:build_id/trace' do + build = get_build(params[:build_id]) + return not_found!(build) unless build + + header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" + content_type 'text/plain' + env['api.format'] = :binary + + trace = build.trace + body trace + end + + # Cancel a specific build of a project + # + # parameters: + # id (required) - the id of a project + # build_id (required) - the id of a build + # example request: + # post /projects/:id/build/:build_id/cancel + post ':id/builds/:build_id/cancel' do + authorize_manage_builds! + + build = get_build(params[:build_id]) + return not_found!(build) unless build + + build.cancel + + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + + # Retry a specific build of a project + # + # parameters: + # id (required) - the id of a project + # build_id (required) - the id of a build + # example request: + # post /projects/:id/build/:build_id/retry + post ':id/builds/:build_id/retry' do + authorize_manage_builds! + + build = get_build(params[:build_id]) + return forbidden!('Build is not retryable') unless build && build.retryable? + + build = Ci::Build.retry(build) + + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + end + + helpers do + def get_build(id) + user_project.builds.find_by(id: id.to_i) + end + + def filter_builds(builds, scope) + return builds if scope.nil? || scope.empty? + + available_statuses = ::CommitStatus::AVAILABLE_STATUSES + scope = + if scope.is_a?(String) + [scope] + elsif scope.is_a?(Hashie::Mash) + scope.values + else + ['unknown'] + end + + unknown = scope - available_statuses + render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? + + builds.where(status: available_statuses && scope) + end + + def authorize_manage_builds! + authorize! :manage_builds, user_project + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e3bc3316ce5..82a75734de0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -366,5 +366,40 @@ module API class TriggerRequest < Grape::Entity expose :id, :variables end + + class Runner < Grape::Entity + expose :id + expose :description + expose :active + expose :is_shared + expose :name + end + + class Build < Grape::Entity + expose :id, :status, :stage, :name, :ref, :tag, :coverage + expose :created_at, :started_at, :finished_at + expose :user, with: User + # TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API + # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255) + expose :download_url do |repo_obj, options| + if options[:user_can_download_artifacts] + repo_obj.download_url + end + end + expose :commit, with: RepoCommit do |repo_obj, _options| + if repo_obj.respond_to?(:commit) + repo_obj.commit.commit_data + end + end + expose :runner, with: Runner + end + + class Trigger < Grape::Entity + expose :token, :created_at, :updated_at, :deleted_at, :last_used + end + + class Variable < Grape::Entity + expose :key, :value + end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a4df810e755..6d2380cf47d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -97,11 +97,9 @@ module API end def paginate(relation) - per_page = params[:per_page].to_i - paginated = relation.page(params[:page]).per(per_page) - add_pagination_headers(paginated, per_page) - - paginated + relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| + add_pagination_headers(data) + end end def authenticate! @@ -289,12 +287,14 @@ module API # file helpers - def uploaded_file!(field, uploads_path) + def uploaded_file(field, uploads_path) if params[field] bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename) return params[field] end + return nil unless params["#{field}.path"] && params["#{field}.name"] + # sanitize file paths # this requires all paths to exist required_attributes! %W(#{field}.path) @@ -327,16 +327,26 @@ module API private - def add_pagination_headers(paginated, per_page) + def add_pagination_headers(paginated_data) + header 'X-Total', paginated_data.total_count.to_s + header 'X-Total-Pages', paginated_data.total_pages.to_s + header 'X-Per-Page', paginated_data.limit_value.to_s + header 'X-Page', paginated_data.current_page.to_s + header 'X-Next-Page', paginated_data.next_page.to_s + header 'X-Prev-Page', paginated_data.prev_page.to_s + header 'Link', pagination_links(paginated_data) + end + + def pagination_links(paginated_data) request_url = request.url.split('?').first links = [] - links << %(<#{request_url}?page=#{paginated.current_page - 1}&per_page=#{per_page}>; rel="prev") unless paginated.first_page? - links << %(<#{request_url}?page=#{paginated.current_page + 1}&per_page=#{per_page}>; rel="next") unless paginated.last_page? - links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first") - links << %(<#{request_url}?page=#{paginated.total_pages}&per_page=#{per_page}>; rel="last") + links << %(<#{request_url}?page=#{paginated_data.current_page - 1}&per_page=#{paginated_data.limit_value}>; rel="prev") unless paginated_data.first_page? + links << %(<#{request_url}?page=#{paginated_data.current_page + 1}&per_page=#{paginated_data.limit_value}>; rel="next") unless paginated_data.last_page? + links << %(<#{request_url}?page=1&per_page=#{paginated_data.limit_value}>; rel="first") + links << %(<#{request_url}?page=#{paginated_data.total_pages}&per_page=#{paginated_data.limit_value}>; rel="last") - header 'Link', links.join(', ') + links.join(', ') end def abilities diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 3efdfe2d46e..174473f5371 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -20,7 +20,19 @@ module API # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) - present paginate(@noteable.notes), with: Entities::Note + + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(@noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: Entities::Note end # Get a single +noteable+ note @@ -35,7 +47,12 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) @note = @noteable.notes.find(params[:note_id]) - present @note, with: Entities::Note + + if @note.cross_reference_not_visible_for?(current_user) + not_found!("Note") + else + present @note, with: Entities::Note + end end # Create a new +noteable+ note diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 2781f1cf191..5e4964f446c 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -43,6 +43,75 @@ module API render_api_error!(errors, 400) end end + + # Get triggers list + # + # Parameters: + # id (required) - The ID of a project + # page (optional) - The page number for pagination + # per_page (optional) - The value of items per page to show + # Example Request: + # GET /projects/:id/triggers + get ':id/triggers' do + authenticate! + authorize_admin_project + + triggers = user_project.triggers.includes(:trigger_requests) + triggers = paginate(triggers) + + present triggers, with: Entities::Trigger + end + + # Get specific trigger of a project + # + # Parameters: + # id (required) - The ID of a project + # token (required) - The `token` of a trigger + # Example Request: + # GET /projects/:id/triggers/:token + get ':id/triggers/:token' do + authenticate! + authorize_admin_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + present trigger, with: Entities::Trigger + end + + # Create trigger + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # POST /projects/:id/triggers + post ':id/triggers' do + authenticate! + authorize_admin_project + + trigger = user_project.triggers.create + + present trigger, with: Entities::Trigger + end + + # Delete trigger + # + # Parameters: + # id (required) - The ID of a project + # token (required) - The `token` of a trigger + # Example Request: + # DELETE /projects/:id/triggers/:token + delete ':id/triggers/:token' do + authenticate! + authorize_admin_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + trigger.destroy + + present trigger, with: Entities::Trigger + end end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 0d7813428e2..fd2128bd179 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -284,10 +284,12 @@ module API authenticated_as_admin! user = User.find_by(id: params[:id]) - if user + if !user + not_found!('User') + elsif !user.ldap_blocked? user.block else - not_found!('User') + forbidden!('LDAP blocked users cannot be modified by the API') end end @@ -299,10 +301,12 @@ module API authenticated_as_admin! user = User.find_by(id: params[:id]) - if user - user.activate - else + if !user not_found!('User') + elsif user.ldap_blocked? + forbidden!('LDAP blocked users cannot be unblocked by the API') + else + user.activate end end end diff --git a/lib/api/variables.rb b/lib/api/variables.rb new file mode 100644 index 00000000000..d9a055f6c92 --- /dev/null +++ b/lib/api/variables.rb @@ -0,0 +1,95 @@ +module API + # Projects variables API + class Variables < Grape::API + before { authenticate! } + before { authorize_admin_project } + + resource :projects do + # Get project variables + # + # Parameters: + # id (required) - The ID of a project + # page (optional) - The page number for pagination + # per_page (optional) - The value of items per page to show + # Example Request: + # GET /projects/:id/variables + get ':id/variables' do + variables = user_project.variables + present paginate(variables), with: Entities::Variable + end + + # Get specific variable of a project + # + # Parameters: + # id (required) - The ID of a project + # key (required) - The `key` of variable + # Example Request: + # GET /projects/:id/variables/:key + get ':id/variables/:key' do + key = params[:key] + variable = user_project.variables.find_by(key: key.to_s) + + return not_found!('Variable') unless variable + + present variable, with: Entities::Variable + end + + # Create a new variable in project + # + # Parameters: + # id (required) - The ID of a project + # key (required) - The key of variable + # value (required) - The value of variable + # Example Request: + # POST /projects/:id/variables + post ':id/variables' do + required_attributes! [:key, :value] + + variable = user_project.variables.create(key: params[:key], value: params[:value]) + + if variable.valid? + present variable, with: Entities::Variable + else + render_validation_error!(variable) + end + end + + # Update existing variable of a project + # + # Parameters: + # id (required) - The ID of a project + # key (optional) - The `key` of variable + # value (optional) - New value for `value` field of variable + # Example Request: + # PUT /projects/:id/variables/:key + put ':id/variables/:key' do + variable = user_project.variables.find_by(key: params[:key].to_s) + + return not_found!('Variable') unless variable + + attrs = attributes_for_keys [:value] + if variable.update(attrs) + present variable, with: Entities::Variable + else + render_validation_error!(variable) + end + end + + # Delete existing variable of a project + # + # Parameters: + # id (required) - The ID of a project + # key (required) - The ID of a variable + # Example Request: + # DELETE /projects/:id/variables/:key + delete ':id/variables/:key' do + variable = user_project.variables.find_by(key: params[:key].to_s) + + return not_found!('Variable') unless variable + variable.destroy + + present variable, with: Entities::Variable + end + end + end +end diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb index ba2866e1efa..0257848b6bc 100644 --- a/lib/banzai/cross_project_reference.rb +++ b/lib/banzai/cross_project_reference.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai # Common methods for ReferenceFilters that support an optional cross-project # reference. diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb index fd4fe024252..905c4c0144e 100644 --- a/lib/banzai/filter.rb +++ b/lib/banzai/filter.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/string/output_safety' -require 'banzai' module Banzai module Filter diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index b2db10e6864..cdbaecf8d90 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # Issues, Merge Requests, Snippets, Commits and Commit Ranges share diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index da4ee80c1b5..856f56fb175 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' require 'uri' diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index e67cd45ab9b..470727ee312 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces commit range references with links. diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 9e57608b483..713a56ba949 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces commit references with links. diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index 86838e1483c..5952a031626 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -1,5 +1,4 @@ require 'action_controller' -require 'banzai' require 'gitlab_emoji' require 'html/pipeline/filter' diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 6136e73c096..edc26386903 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces external issue tracker references with links. diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index ac87b9820af..8d368f3b9e7 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' module Banzai diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb new file mode 100644 index 00000000000..fe01dae4850 --- /dev/null +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -0,0 +1,151 @@ +require 'banzai' +require 'html/pipeline/filter' + +module Banzai + module Filter + # HTML Filter for parsing Gollum's tags in HTML. It's only parses the + # following tags: + # + # - Link to internal pages: + # + # * [[Bug Reports]] + # * [[How to Contribute|Contributing]] + # + # - Link to external resources: + # + # * [[http://en.wikipedia.org/wiki/Git_(software)]] + # * [[Git|http://en.wikipedia.org/wiki/Git_(software)]] + # + # - Link internal images, the special attributes will be ignored: + # + # * [[images/logo.png]] + # * [[images/logo.png|alt=Logo]] + # + # - Link external images, the special attributes will be ignored: + # + # * [[http://example.com/images/logo.png]] + # * [[http://example.com/images/logo.png|alt=Logo]] + # + # Based on Gollum::Filter::Tags + # + # Context options: + # :project_wiki (required) - Current project wiki. + # + class GollumTagsFilter < HTML::Pipeline::Filter + include ActionView::Helpers::TagHelper + + # Pattern to match tags content that should be parsed in HTML. + # + # Gollum's tags have been made to resemble the tags of other markups, + # especially MediaWiki. The basic syntax is: + # + # [[tag]] + # + # Some tags will accept attributes which are separated by pipe + # symbols.Some attributes must precede the tag and some must follow it: + # + # [[prefix-attribute|tag]] + # [[tag|suffix-attribute]] + # + # See https://github.com/gollum/gollum/wiki + # + # Rubular: http://rubular.com/r/7dQnE5CUCH + TAGS_PATTERN = %r{\[\[(.+?)\]\]} + + # Pattern to match allowed image extensions + ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i + + def call + search_text_nodes(doc).each do |node| + content = node.content + + next unless content.match(TAGS_PATTERN) + + html = process_tag($1) + + if html && html != node.content + node.replace(html) + end + end + + doc + end + + private + + # Process a single tag into its final HTML form. + # + # tag - The String tag contents (the stuff inside the double brackets). + # + # Returns the String HTML version of the tag. + def process_tag(tag) + parts = tag.split('|') + + return if parts.size.zero? + + process_image_tag(parts) || process_page_link_tag(parts) + end + + # Attempt to process the tag as an image tag. + # + # tag - The String tag contents (the stuff inside the double brackets). + # + # Returns the String HTML if the tag is a valid image tag or nil + # if it is not. + def process_image_tag(parts) + content = parts[0].strip + + return unless image?(content) + + if url?(content) + path = content + elsif file = project_wiki.find_file(content) + path = ::File.join project_wiki_base_path, file.path + end + + if path + content_tag(:img, nil, src: path) + end + end + + def image?(path) + path =~ ALLOWED_IMAGE_EXTENSIONS + end + + def url?(path) + path.start_with?(*%w(http https)) + end + + # Attempt to process the tag as a page link tag. + # + # tag - The String tag contents (the stuff inside the double brackets). + # + # Returns the String HTML if the tag is a valid page link tag or nil + # if it is not. + def process_page_link_tag(parts) + if parts.size == 1 + url = parts[0].strip + else + name, url = *parts.compact.map(&:strip) + end + + content_tag(:a, name || url, href: url) + end + + def project_wiki + context[:project_wiki] + end + + def project_wiki_base_path + project_wiki && project_wiki.wiki_base_path + end + + # Ensure that a :project_wiki key exists in context + # + # Note that while the key might exist, its value could be nil! + def validate + needs :project_wiki + end + end + end +end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 51180cb901a..9f08aa36e8b 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces issue references with links. References to diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index a3a7a23c1e6..95e7d209119 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces label references with links. diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index d09cf41df39..0659fed1419 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' module Banzai diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index 755b946a34b..57c71708992 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces merge request references with links. References diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index 66f77902319..7141ed7c9bd 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' module Banzai diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 5dd6d2fe3c7..20bd4f7ee6e 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/string/output_safety' -require 'banzai' require 'html/pipeline/filter' module Banzai diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb index bef04112919..86d484feb90 100644 --- a/lib/banzai/filter/reference_gatherer_filter.rb +++ b/lib/banzai/filter/reference_gatherer_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' module Banzai diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 66f166939e4..41380627d39 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' require 'uri' diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index d03e3ae4b3c..3f49d492f2f 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' require 'html/pipeline/sanitization_filter' diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index 1ad5df96f85..c870a42f741 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces snippet references with links. References to diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index c889cc1e97c..8c5855e5ffc 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' require 'rouge/plugins/redcarpet' diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 9b3e67206d5..4056dcd6d64 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' module Banzai diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb index d0ce13003a5..66608c9859c 100644 --- a/lib/banzai/filter/task_list_filter.rb +++ b/lib/banzai/filter/task_list_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'task_list/filter' module Banzai diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb index 1a1d0aad8ca..f642aee0967 100644 --- a/lib/banzai/filter/upload_link_filter.rb +++ b/lib/banzai/filter/upload_link_filter.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline/filter' require 'uri' diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 964ab60f614..24f16f8b547 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces user or group references with links. diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb index 073ec5d9801..1095b4debc7 100644 --- a/lib/banzai/lazy_reference.rb +++ b/lib/banzai/lazy_reference.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai class LazyReference def self.load(refs) diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb index 4e017809d9d..142a9962eb1 100644 --- a/lib/banzai/pipeline.rb +++ b/lib/banzai/pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline def self.[](name) diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb index 5e76a817be5..f1331c0ebf9 100644 --- a/lib/banzai/pipeline/asciidoc_pipeline.rb +++ b/lib/banzai/pipeline/asciidoc_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class AsciidocPipeline < BasePipeline diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb index 957f352aec5..9694e4bc23f 100644 --- a/lib/banzai/pipeline/atom_pipeline.rb +++ b/lib/banzai/pipeline/atom_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class AtomPipeline < FullPipeline diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb index cd30009e5c0..db5177db7b3 100644 --- a/lib/banzai/pipeline/base_pipeline.rb +++ b/lib/banzai/pipeline/base_pipeline.rb @@ -1,4 +1,3 @@ -require 'banzai' require 'html/pipeline' module Banzai diff --git a/lib/banzai/pipeline/combined_pipeline.rb b/lib/banzai/pipeline/combined_pipeline.rb index f3bf1809d18..9485199132e 100644 --- a/lib/banzai/pipeline/combined_pipeline.rb +++ b/lib/banzai/pipeline/combined_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline module CombinedPipeline diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb index 94c2cb165a5..20e24ace352 100644 --- a/lib/banzai/pipeline/description_pipeline.rb +++ b/lib/banzai/pipeline/description_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class DescriptionPipeline < FullPipeline diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb index 14356145a35..e47c384afc1 100644 --- a/lib/banzai/pipeline/email_pipeline.rb +++ b/lib/banzai/pipeline/email_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class EmailPipeline < FullPipeline diff --git a/lib/banzai/pipeline/full_pipeline.rb b/lib/banzai/pipeline/full_pipeline.rb index 72395a5d50e..d47ddfda4be 100644 --- a/lib/banzai/pipeline/full_pipeline.rb +++ b/lib/banzai/pipeline/full_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline) diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 838155e8831..b7a38ea8427 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class GfmPipeline < BasePipeline diff --git a/lib/banzai/pipeline/note_pipeline.rb b/lib/banzai/pipeline/note_pipeline.rb index 89335143852..7890f20f716 100644 --- a/lib/banzai/pipeline/note_pipeline.rb +++ b/lib/banzai/pipeline/note_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class NotePipeline < FullPipeline diff --git a/lib/banzai/pipeline/plain_markdown_pipeline.rb b/lib/banzai/pipeline/plain_markdown_pipeline.rb index 998fd75daa2..3fbc681457b 100644 --- a/lib/banzai/pipeline/plain_markdown_pipeline.rb +++ b/lib/banzai/pipeline/plain_markdown_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class PlainMarkdownPipeline < BasePipeline diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb index 148f24b6ce1..bd338c045f3 100644 --- a/lib/banzai/pipeline/post_process_pipeline.rb +++ b/lib/banzai/pipeline/post_process_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class PostProcessPipeline < BasePipeline diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb index 4f9bc9fcccc..eaddccd30a5 100644 --- a/lib/banzai/pipeline/reference_extraction_pipeline.rb +++ b/lib/banzai/pipeline/reference_extraction_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class ReferenceExtractionPipeline < BasePipeline diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index a3c9d4f43aa..8b84ab401df 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class SingleLinePipeline < GfmPipeline diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb new file mode 100644 index 00000000000..50b5450e70b --- /dev/null +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -0,0 +1,11 @@ +require 'banzai' + +module Banzai + module Pipeline + class WikiPipeline < FullPipeline + def self.filters + super.insert(1, Filter::GollumTagsFilter) + end + end + end +end diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb index 2c197d31898..f4079538ec5 100644 --- a/lib/banzai/reference_extractor.rb +++ b/lib/banzai/reference_extractor.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 15faa6edd84..690bbf97a89 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -20,7 +20,7 @@ module Ci if build update_runner_info - present build, with: Entities::Build + present build, with: Entities::BuildDetails else not_found! end @@ -78,11 +78,13 @@ module Ci # Parameters: # id (required) - The ID of a build # token (required) - The build authorization token - # file (required) - The uploaded file + # file (required) - Artifacts file # Parameters (accelerated by GitLab Workhorse): # file.path - path to locally stored body (generated by Workhorse) # file.name - real filename as send in Content-Disposition # file.type - real content type as send in Content-Type + # metadata.path - path to locally stored body (generated by Workhorse) + # metadata.name - filename (generated by Workhorse) # Headers: # BUILD-TOKEN (required) - The build authorization token, the same as token # Body: @@ -96,13 +98,20 @@ module Ci build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) - forbidden!('build is not running') unless build.running? + forbidden!('Build is not running!') unless build.running? + + artifacts_upload_path = ArtifactUploader.artifacts_upload_path + artifacts = uploaded_file(:file, artifacts_upload_path) + metadata = uploaded_file(:metadata, artifacts_upload_path) + + bad_request!('Missing artifacts file!') unless artifacts + file_to_large! unless artifacts.size < max_artifacts_size - file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path) - file_to_large! unless file.size < max_artifacts_size + build.artifacts_file = artifacts + build.artifacts_metadata = metadata - if build.update_attributes(artifacts_file: file) - present build, with: Entities::Build + if build.save + present(build, with: Entities::BuildDetails) else render_validation_error!(build) end @@ -148,6 +157,7 @@ module Ci not_found! unless build authenticate_build_token!(build) build.remove_artifacts_file! + build.remove_artifacts_metadata! end end end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index e4ac0545ea2..b25e0e573a8 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -16,10 +16,19 @@ module Ci end class Build < Grape::Entity - expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url, - :before_sha, :allow_git_fetch, :project_name - + expose :id, :ref, :tag, :sha, :status expose :name, :token, :stage + expose :project_id + expose :project_name + expose :artifacts_file, using: ArtifactFile, if: lambda { |build, opts| build.artifacts? } + end + + class BuildDetails < Build + expose :commands + expose :repo_url + expose :before_sha + expose :allow_git_fetch + expose :token expose :options do |model| model.options @@ -30,7 +39,7 @@ module Ci end expose :variables - expose :artifacts_file, using: ArtifactFile + expose :depends_on_builds, using: Build end class Runner < Grape::Entity diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb new file mode 100644 index 00000000000..1344f5d120b --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -0,0 +1,109 @@ +require 'zlib' +require 'json' + +module Gitlab + module Ci + module Build + module Artifacts + class Metadata + class ParserError < StandardError; end + + VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/ + INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)} + + attr_reader :file, :path, :full_version + + def initialize(file, path) + @file, @path = file, path + @full_version = read_version + end + + def version + @full_version.match(VERSION_PATTERN)[1] + end + + def errors + gzip do |gz| + read_string(gz) # version + errors = read_string(gz) + raise ParserError, 'Errors field not found!' unless errors + + begin + JSON.parse(errors) + rescue JSON::ParserError + raise ParserError, 'Invalid errors field!' + end + end + end + + def find_entries! + gzip do |gz| + 2.times { read_string(gz) } # version and errors fields + match_entries(gz) + end + end + + def to_entry + entries = find_entries! + Entry.new(@path, entries) + end + + private + + def match_entries(gz) + entries = {} + match_pattern = %r{^#{Regexp.escape(@path)}[^/]*/?$} + + until gz.eof? do + begin + path = read_string(gz).force_encoding('UTF-8') + meta = read_string(gz).force_encoding('UTF-8') + + next unless path.valid_encoding? && meta.valid_encoding? + next unless path =~ match_pattern + next if path =~ INVALID_PATH_PATTERN + + entries[path] = JSON.parse(meta, symbolize_names: true) + rescue JSON::ParserError, Encoding::CompatibilityError + next + end + end + + entries + end + + def read_version + gzip do |gz| + version_string = read_string(gz) + + unless version_string + raise ParserError, 'Artifacts metadata file empty!' + end + + unless version_string =~ VERSION_PATTERN + raise ParserError, 'Invalid version!' + end + + version_string.chomp + end + end + + def read_uint32(gz) + binary = gz.read(4) + binary.unpack('L>')[0] if binary + end + + def read_string(gz) + string_size = read_uint32(gz) + return nil unless string_size + gz.read(string_size) + end + + def gzip(&block) + Zlib::GzipReader.open(@file, &block) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb new file mode 100644 index 00000000000..25b71fc3275 --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -0,0 +1,119 @@ +module Gitlab + module Ci::Build::Artifacts + class Metadata + ## + # Class that represents an entry (path and metadata) to a file or + # directory in GitLab CI Build Artifacts binary file / archive + # + # This is IO-operations safe class, that does similar job to + # Ruby's Pathname but without the risk of accessing filesystem. + # + # This class is working only with UTF-8 encoded paths. + # + class Entry + attr_reader :path, :entries + attr_accessor :name + + def initialize(path, entries) + @path = path.dup.force_encoding('UTF-8') + @entries = entries + + if path.include?("\0") + raise ArgumentError, 'Path contains zero byte character!' + end + + unless path.valid_encoding? + raise ArgumentError, 'Path contains non-UTF-8 byte sequence!' + end + end + + def directory? + blank_node? || @path.end_with?('/') + end + + def file? + !directory? + end + + def has_parent? + nodes > 0 + end + + def parent + return nil unless has_parent? + self.class.new(@path.chomp(basename), @entries) + end + + def basename + (directory? && !blank_node?) ? name + '/' : name + end + + def name + @name || @path.split('/').last.to_s + end + + def children + return [] unless directory? + return @children if @children + + child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$} + @children = select_entries { |path| path =~ child_pattern } + end + + def directories(opts = {}) + return [] unless directory? + dirs = children.select(&:directory?) + return dirs unless has_parent? && opts[:parent] + + dotted_parent = parent + dotted_parent.name = '..' + dirs.prepend(dotted_parent) + end + + def files + return [] unless directory? + children.select(&:file?) + end + + def metadata + @entries[@path] || {} + end + + def nodes + @path.count('/') + (file? ? 1 : 0) + end + + def blank_node? + @path.empty? # "" is considered to be './' + end + + def exists? + blank_node? || @entries.include?(@path) + end + + def empty? + children.empty? + end + + def to_s + @path + end + + def ==(other) + @path == other.path && @entries == other.entries + end + + def inspect + "#{self.class.name}: #{@path}" + end + + private + + def select_entries + selected = @entries.select { |path, _metadata| yield path } + selected.map { |path, _metadata| self.class.new(path, @entries) } + end + end + end + end +end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 7015fe36c3d..516e59b87a3 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -56,8 +56,9 @@ module Gitlab private def filename?(line) - line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b', - '--- /tmp/diffy', '+++ /tmp/diffy') + line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b', + '+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit + '--- /tmp/diffy', '+++ /tmp/diffy') end def identification_type(line) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 2b0afbc7b39..18929b9113b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -1,6 +1,8 @@ module Gitlab module GithubImport class Importer + include Gitlab::ShellAdapter + attr_reader :project, :client def initialize(project) @@ -12,10 +14,7 @@ module Gitlab end def execute - import_issues - import_pull_requests - - true + import_issues && import_pull_requests && import_wiki end private @@ -34,6 +33,10 @@ module Gitlab end end end + + true + rescue ActiveRecord::RecordInvalid + false end def import_pull_requests @@ -48,6 +51,10 @@ module Gitlab import_comments_on_diff(pull_request.number, merge_request) end end + + true + rescue ActiveRecord::RecordInvalid + false end def import_comments(issue_number, noteable) @@ -66,6 +73,18 @@ module Gitlab noteable.notes.create!(comment.attributes) end end + + def import_wiki + unless project.wiki_enabled? + wiki = WikiFormatter.new(project) + gitlab_shell.import_repository(wiki.path_with_namespace, wiki.import_url) + project.update_attribute(:wiki_enabled, true) + end + + true + rescue Gitlab::Shell::Error + false + end end end end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index 8c27ebd1ce8..474927069a5 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -20,7 +20,8 @@ module Gitlab visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, import_type: "github", import_source: repo.full_name, - import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") + import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"), + wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later ).execute project.create_import_data(data: { "github_session" => session_data } ) diff --git a/lib/gitlab/github_import/wiki_formatter.rb b/lib/gitlab/github_import/wiki_formatter.rb new file mode 100644 index 00000000000..6c592ff469c --- /dev/null +++ b/lib/gitlab/github_import/wiki_formatter.rb @@ -0,0 +1,19 @@ +module Gitlab + module GithubImport + class WikiFormatter + attr_reader :project + + def initialize(project) + @project = project + end + + def path_with_namespace + "#{project.path_with_namespace}.wiki" + end + + def import_url + project.import_url.sub(/\.git\z/, ".wiki.git") + end + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index b2bdbc10d7f..da4435c7308 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -37,15 +37,15 @@ module Gitlab # 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) - user.block + user.ldap_block false else - user.activate if user.blocked? && !ldap_config.block_auto_created_users + user.activate if user.ldap_blocked? true end else # Block the user if they no longer exist in LDAP/AD - user.block + user.ldap_block false end rescue diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb index 8f3f43c0e91..699d8b9fc07 100644 --- a/lib/gitlab/markdown/pipeline.rb +++ b/lib/gitlab/markdown/pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Gitlab module Markdown class Pipeline diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index f1a362f5303..e3d2cc65a8f 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -141,9 +141,12 @@ module Gitlab username = auth_hash.username email = auth_hash.email end + + name = auth_hash.name + name = ::Namespace.clean_path(username) if name.strip.empty? { - name: auth_hash.name, + name: name, username: ::Namespace.clean_path(username), email: email, password: auth_hash.password, diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 4164e998dd1..4d830aa45e1 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor < Banzai::ReferenceExtractor diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index 8c63877e51c..d33b5b31e18 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -4,6 +4,9 @@ end String.disable_colorization = true unless STDOUT.isatty +# Prevent StateMachine warnings from outputting during a cron task +StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] + namespace :gitlab do # Ask if the user wants to continue diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb new file mode 100644 index 00000000000..c131d22a30a --- /dev/null +++ b/spec/controllers/admin/identities_controller_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Admin::IdentitiesController do + let(:admin) { create(:admin) } + before { sign_in(admin) } + + describe 'UPDATE identity' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } + + it 'repairs ldap blocks' do + expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute) + + put :update, user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' } + end + end + + describe 'DELETE identity' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } + + it 'repairs ldap blocks' do + expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute) + + delete :destroy, user_id: user.username, id: user.ldap_identity.id + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 8b7af4d3a0a..5b1f65d7aff 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -34,17 +34,34 @@ describe Admin::UsersController do end describe 'PUT unblock/:id' do - let(:user) { create(:user) } - - before do - user.block + context 'ldap blocked users' do + let(:user) { create(:omniauth_user, provider: 'ldapmain') } + + before do + user.ldap_block + end + + it 'will not unblock user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_truthy + expect(flash[:alert]).to eq 'This user cannot be unlocked manually from GitLab' + end end - it 'unblocks user' do - put :unblock, id: user.username - user.reload - expect(user.blocked?).to be_falsey - expect(flash[:notice]).to eq 'Successfully unblocked' + context 'manually blocked users' do + let(:user) { create(:user) } + + before do + user.block + end + + it 'unblocks user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_falsey + expect(flash[:notice]).to eq 'Successfully unblocked' + end end end diff --git a/spec/controllers/sent_notification_controller_spec.rb b/spec/controllers/sent_notification_controller_spec.rb new file mode 100644 index 00000000000..9ced397bd4a --- /dev/null +++ b/spec/controllers/sent_notification_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe SentNotificationsController, type: :controller do + let(:user) { create(:user) } + let(:issue) { create(:issue, author: user) } + let(:sent_notification) { create(:sent_notification, noteable: issue) } + + describe 'GET #unsubscribe' do + it 'returns a 404 when calling without existing id' do + get(:unsubscribe, id: '0' * 32) + + expect(response.status).to be 404 + end + + context 'calling with id' do + it 'shows a flash message to the user' do + get(:unsubscribe, id: sent_notification.reply_key) + + expect(response.status).to be 302 + + expect(response).to redirect_to new_user_session_path + expect(controller).to set_flash[:notice].to(/unsubscribed/).now + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index d6b4efa9a03..2a81684dfcf 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -212,4 +212,11 @@ FactoryGirl.define do provider 'ldapmain' extern_uid 'my-ldap-id' end + + factory :sent_notification do + project + recipient factory: :user + noteable factory: :issue + reply_key "0123456789abcdef" * 2 + end end diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index ea0039d39e6..978a7d4cecb 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -6,7 +6,6 @@ # message :text not null # starts_at :datetime # ends_at :datetime -# alert_type :integer # created_at :datetime # updated_at :datetime # color :string(255) @@ -18,10 +17,17 @@ FactoryGirl.define do factory :broadcast_message do message "MyText" - starts_at "2013-11-12 13:43:25" - ends_at "2013-11-12 13:43:25" - alert_type 1 - color "#555555" - font "#BBBBBB" + starts_at Date.today + ends_at Date.tomorrow + + trait :expired do + starts_at 5.days.ago + ends_at 3.days.ago + end + + trait :future do + starts_at 5.days.from_now + ends_at 6.days.from_now + end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f76e826f138..d2db77f6286 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -30,6 +30,7 @@ FactoryGirl.define do name 'test' ref 'master' tag false + created_at 'Di 29. Okt 09:50:00 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' commands 'ls -a' @@ -42,6 +43,10 @@ FactoryGirl.define do commit factory: :ci_commit + trait :canceled do + status 'canceled' + end + after(:build) do |build, evaluator| build.project = build.commit.project end @@ -54,5 +59,11 @@ FactoryGirl.define do factory :ci_build_tag do tag true end + + factory :ci_build_with_trace do + after(:create) do |build, evaluator| + build.trace = 'BUILD TRACE' + end + end end end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index db053c610cd..2c0d004d267 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -3,6 +3,8 @@ FactoryGirl.define do factory :ci_trigger_request, class: Ci::TriggerRequest do factory :ci_trigger_request_with_variables do + trigger factory: :ci_trigger + variables do { TRIGGER_KEY: 'TRIGGER_VALUE' diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb new file mode 100644 index 00000000000..8f62d64411b --- /dev/null +++ b/spec/factories/ci/variables.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: ci_variables +# +# id :integer not null, primary key +# project_id :integer not null +# key :string(255) +# value :text +# encrypted_value :text +# encrypted_value_salt :string(255) +# encrypted_value_iv :string(255) +# gl_project_id :integer +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_variable, class: Ci::Variable do + sequence(:key) { |n| "VARIABLE_#{n}" } + value 'VARIABLE_VALUE' + end +end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 240e56839df..d37bd103714 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -80,7 +80,11 @@ describe "Builds" do visit namespace_project_build_path(@project.namespace, @project, @build) end - it { expect(page).to have_content 'Download artifacts' } + it 'has button to download artifacts' do + page.within('.artifacts') do + expect(page).to have_content 'Download' + end + end end end @@ -111,7 +115,7 @@ describe "Builds" do before do @build.update_attributes(artifacts_file: artifacts_file) visit namespace_project_build_path(@project.namespace, @project, @build) - click_link 'Download artifacts' + page.within('.artifacts') { click_link 'Download' } end it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index e836d81c40b..12fd8d37210 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do end end - context 'default pipeline' do - before(:all) do - @feat = MarkdownFeature.new + before(:all) do + @feat = MarkdownFeature.new - # `markdown` helper expects a `@project` variable - @project = @feat.project + # `markdown` helper expects a `@project` variable + @project = @feat.project + end + context 'default pipeline' do + before(:all) do @html = markdown(@feat.raw_markdown) end @@ -221,6 +223,57 @@ describe 'GitLab Markdown', feature: true do end end + context 'wiki pipeline' do + before do + @project_wiki = @feat.project_wiki + + 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) + + @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) + end + + it_behaves_like 'all pipelines' + + it 'includes RelativeLinkFilter' do + expect(doc).not_to parse_relative_links + end + + it 'includes EmojiFilter' do + expect(doc).to parse_emoji + end + + it 'includes TableOfContentsFilter' do + expect(doc).to create_header_links + end + + it 'includes AutolinkFilter' do + expect(doc).to create_autolinks + end + + it 'includes all reference filters' do + aggregate_failures do + expect(doc).to reference_users + expect(doc).to reference_issues + expect(doc).to reference_merge_requests + expect(doc).to reference_snippets + expect(doc).to reference_commit_ranges + expect(doc).to reference_commits + expect(doc).to reference_labels + expect(doc).to reference_milestones + end + end + + it 'includes TaskListFilter' do + expect(doc).to parse_task_lists + end + + it 'includes GollumTagsFilter' do + expect(doc).to parse_gollum_tags + end + end + # Fake a `current_user` helper def current_user @feat.user diff --git a/spec/fixtures/ci_build_artifacts.zip b/spec/fixtures/ci_build_artifacts.zip Binary files differnew file mode 100644 index 00000000000..dae976d918e --- /dev/null +++ b/spec/fixtures/ci_build_artifacts.zip diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz Binary files differnew file mode 100644 index 00000000000..fe9d4c8c661 --- /dev/null +++ b/spec/fixtures/ci_build_artifacts_metadata.gz diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 0620096d689..fe6d42acee2 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -230,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - [ ] Incomplete sub-task 2 - [x] Complete sub-task 1 - [X] Complete task 2 + +#### Gollum Tags + +- [[linked-resource]] +- [[link-text|linked-resource]] +- [[http://example.com]] +- [[link-text|http://example.com/pdfs/gollum.pdf]] +- [[images/example.jpg]] +- [[http://example.com/images/example.jpg]] diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index c7c6f45d144..157cc4665a2 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -1,22 +1,60 @@ require 'spec_helper' describe BroadcastMessagesHelper do - describe 'broadcast_styling' do - let(:broadcast_message) { double(color: '', font: '') } + describe 'broadcast_message' do + it 'returns nil when no current message' do + expect(helper.broadcast_message(nil)).to be_nil + end + + it 'includes the current message' do + current = double(message: 'Current Message') + + allow(helper).to receive(:broadcast_message_style).and_return(nil) + + expect(helper.broadcast_message(current)).to include 'Current Message' + end + + it 'includes custom style' do + current = double(message: 'Current Message') + + allow(helper).to receive(:broadcast_message_style).and_return('foo') + + expect(helper.broadcast_message(current)).to include 'style="foo"' + end + end + + describe 'broadcast_message_style' do + it 'defaults to no style' do + broadcast_message = spy + + expect(helper.broadcast_message_style(broadcast_message)).to eq '' + end + + it 'allows custom style' do + broadcast_message = double(color: '#f2dede', font: '#b94a48') + + expect(helper.broadcast_message_style(broadcast_message)). + to match('background-color: #f2dede; color: #b94a48') + end + end + + describe 'broadcast_message_status' do + it 'returns Active' do + message = build(:broadcast_message) + + expect(helper.broadcast_message_status(message)).to eq 'Active' + end + + it 'returns Expired' do + message = build(:broadcast_message, :expired) - context "default style" do - it "should have no style" do - expect(broadcast_styling(broadcast_message)).to eq '' - end + expect(helper.broadcast_message_status(message)).to eq 'Expired' end - context "customized style" do - let(:broadcast_message) { double(color: "#f2dede", font: '#b94a48') } + it 'returns Pending' do + message = build(:broadcast_message, :future) - it "should have a customized style" do - expect(broadcast_styling(broadcast_message)). - to match('background-color: #f2dede; color: #b94a48') - end + expect(helper.broadcast_message_status(message)).to eq 'Pending' end end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 762ec25c4f5..9a05b21335c 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do before do @wiki = double('WikiPage') allow(@wiki).to receive(:content).and_return('wiki content') + helper.instance_variable_set(:@project_wiki, @wiki) end - it "should use GitLab Flavored Markdown for markdown files" do + it "should use Wiki pipeline for markdown files" do allow(@wiki).to receive(:format).and_return(:markdown) - expect(helper).to receive(:markdown).with('wiki content') + expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki) helper.render_wiki_content(@wiki) end diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb new file mode 100644 index 00000000000..38baa819957 --- /dev/null +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe Banzai::Filter::GollumTagsFilter, lib: true do + include FilterSpecHelper + + let(:project) { create(:project) } + let(:user) { double } + let(:project_wiki) { ProjectWiki.new(project, user) } + + describe 'validation' do + it 'ensure that a :project_wiki key exists in context' do + expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :project_wiki" + end + end + + context 'linking internal images' do + it 'creates img tag if image exists' do + file = Gollum::File.new(project_wiki.wiki) + expect(file).to receive(:path).and_return('images/image.jpg') + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file) + + tag = '[[images/image.jpg]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg" + end + + it 'does not creates img tag if image does not exist' do + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(nil) + + tag = '[[images/image.jpg]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.css('img').size).to eq 0 + end + end + + context 'linking external images' do + it 'creates img tag for valid URL' do + tag = '[[http://example.com/image.jpg]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg" + end + + it 'does not creates img tag for invalid URL' do + tag = '[[http://example.com/image.pdf]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.css('img').size).to eq 0 + end + end + + context 'linking external resources' do + it "the created link's text will be equal to the resource's text" do + tag = '[[http://example.com]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'http://example.com' + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it "the created link's text will be link-text" do + tag = '[[link-text|http://example.com/pdfs/gollum.pdf]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'link-text' + expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf' + end + end + + context 'linking internal resources' do + it "the created link's text will be equal to the resource's text" do + tag = '[[wiki-slug]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'wiki-slug' + expect(doc.at_css('a')['href']).to eq 'wiki-slug' + 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) + + expect(doc.at_css('a').text).to eq 'link-text' + expect(doc.at_css('a')['href']).to eq 'wiki-slug' + end + end +end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb new file mode 100644 index 00000000000..41257103ead --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do + let(:entries) do + { 'path/' => {}, + 'path/dir_1/' => {}, + 'path/dir_1/file_1' => {}, + 'path/dir_1/file_b' => {}, + 'path/dir_1/subdir/' => {}, + 'path/dir_1/subdir/subfile' => {}, + 'path/second_dir' => {}, + 'path/second_dir/dir_3/file_2' => {}, + 'path/second_dir/dir_3/file_3'=> {}, + 'another_directory/'=> {}, + 'another_file' => {}, + '/file/with/absolute_path' => {} } + end + + def path(example) + entry(example.metadata[:path]) + end + + def entry(path) + described_class.new(path, entries) + end + + describe '/file/with/absolute_path', path: '/file/with/absolute_path' do + subject { |example| path(example) } + + it { is_expected.to be_file } + it { is_expected.to have_parent } + + describe '#basename' do + subject { |example| path(example).basename } + it { is_expected.to eq 'absolute_path' } + end + end + + describe 'path/dir_1/', path: 'path/dir_1/' do + subject { |example| path(example) } + it { is_expected.to have_parent } + it { is_expected.to be_directory } + + describe '#basename' do + subject { |example| path(example).basename } + it { is_expected.to eq 'dir_1/' } + end + + describe '#name' do + subject { |example| path(example).name } + it { is_expected.to eq 'dir_1' } + end + + describe '#parent' do + subject { |example| path(example).parent } + it { is_expected.to eq entry('path/') } + end + + describe '#children' do + subject { |example| path(example).children } + + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/file_1'), + entry('path/dir_1/file_b'), + entry('path/dir_1/subdir/') + end + end + + describe '#files' do + subject { |example| path(example).files } + + it { is_expected.to all(be_file) } + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/file_1'), + entry('path/dir_1/file_b') + end + end + + describe '#directories' do + context 'without options' do + subject { |example| path(example).directories } + + it { is_expected.to all(be_directory) } + it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to contain_exactly entry('path/dir_1/subdir/') } + end + + context 'with option parent: true' do + subject { |example| path(example).directories(parent: true) } + + it { is_expected.to all(be_directory) } + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/subdir/'), + entry('path/') + end + end + + describe '#nodes' do + subject { |example| path(example).nodes } + it { is_expected.to eq 2 } + end + + describe '#exists?' do + subject { |example| path(example).exists? } + it { is_expected.to be true } + end + + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be false } + end + end + end + + describe 'empty path', path: '' do + subject { |example| path(example) } + it { is_expected.to_not have_parent } + + describe '#children' do + subject { |example| path(example).children } + it { expect(subject.count).to eq 3 } + end + + end + + describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do + describe '#nodes' do + subject { |example| path(example).nodes } + it { is_expected.to eq 4 } + end + end + + describe 'non-existent/', path: 'non-existent/' do + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be true } + end + + describe '#exists?' do + subject { |example| path(example).exists? } + it { is_expected.to be false } + end + end + + describe 'another_directory/', path: 'another_directory/' do + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be true } + end + end + + describe '#metadata' do + let(:entries) do + { 'path/' => { name: '/path/' }, + 'path/file1' => { name: '/path/file1' }, + 'path/file2' => { name: '/path/file2' } } + end + + subject do + described_class.new('path/file1', entries).metadata[:name] + end + + it { is_expected.to eq '/path/file1' } + end +end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb new file mode 100644 index 00000000000..828eedfa7b0 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Metadata do + def metadata(path = '') + described_class.new(metadata_file_path, path) + end + + let(:metadata_file_path) do + Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + end + + context 'metadata file exists' do + describe '#find_entries! empty string' do + subject { metadata('').find_entries! } + + it 'matches correct paths' do + expect(subject.keys).to contain_exactly 'ci_artifacts.txt', + 'other_artifacts_0.1.2/', + 'rails_sample.jpg', + 'tests_encoding/' + end + + it 'matches metadata for every path' do + expect(subject.keys.count).to eq 4 + end + + it 'return Hashes for each metadata' do + expect(subject.values).to all(be_kind_of(Hash)) + end + end + + describe '#find_entries! other_artifacts_0.1.2/' do + subject { metadata('other_artifacts_0.1.2/').find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/' + end + end + + describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do + subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + end + + describe '#to_entry' do + subject { metadata('').to_entry } + it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) } + end + + describe '#full_version' do + subject { metadata('').full_version } + it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' } + end + + describe '#version' do + subject { metadata('').version } + it { is_expected.to eq '0.0.1' } + end + + describe '#errors' do + subject { metadata('').errors } + it { is_expected.to eq({}) } + end + end + + context 'metadata file does not exist' do + let(:metadata_file_path) { '' } + + describe '#find_entries!' do + it 'raises error' do + expect { metadata.find_entries! }.to raise_error(Errno::ENOENT) + end + end + end +end diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb new file mode 100644 index 00000000000..aed2aa39e3a --- /dev/null +++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::WikiFormatter, lib: true do + let(:project) do + create(:project, namespace: create(:namespace, path: 'gitlabhq'), + import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git') + end + + subject(:wiki) { described_class.new(project)} + + describe '#path_with_namespace' do + it 'appends .wiki to project path' do + expect(wiki.path_with_namespace).to eq 'gitlabhq/gitlabhq.wiki' + end + end + + describe '#import_url' do + it 'returns URL of the wiki repository' do + expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git' + end + end +end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index a628d0c0157..32a19bf344b 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -13,64 +13,58 @@ describe Gitlab::LDAP::Access, lib: true do end it { is_expected.to be_falsey } - + it 'should block user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).to be_ldap_blocked end end context 'when the user is found' do before do - allow(Gitlab::LDAP::Person). - to receive(:find_by_dn).and_return(:ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) end context 'and the user is disabled via active directory' do before do - allow(Gitlab::LDAP::Person). - to receive(:disabled_via_active_directory?).and_return(true) + allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true) end it { is_expected.to be_falsey } - it "should block user in GitLab" do + it 'should block user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).to be_ldap_blocked end end context 'and has no disabled flag in active diretory' do before do - user.block - - allow(Gitlab::LDAP::Person). - to receive(:disabled_via_active_directory?).and_return(false) + allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) end it { is_expected.to be_truthy } context 'when auto-created users are blocked' do - before do - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(true) + user.block end - it "does not unblock user in GitLab" do + it 'does not unblock user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic end end - context "when auto-created users are not blocked" do - + context 'when auto-created users are not blocked' do before do - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(false) + user.ldap_block end - it "should unblock user in GitLab" do + it 'should unblock user in GitLab' do access.allowed? expect(user).not_to be_blocked end @@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do context 'without ActiveDirectory enabled' do before do allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:active_directory).and_return(false) + allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false) end it { is_expected.to be_truthy } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 154901a2fbc..8f86c491d3f 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -104,6 +104,14 @@ describe Notify do it { is_expected.to have_body_text /View Commit/ } end + shared_examples 'an unsubscribeable thread' do + it { is_expected.to have_body_text /unsubscribe/ } + end + + shared_examples "a user cannot unsubscribe through footer link" do + it { is_expected.not_to have_body_text /unsubscribe/ } + end + describe 'for new users, the email' do let(:example_site_path) { root_path } let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } @@ -115,6 +123,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'a new user email', new_user_address it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' it 'contains the password text' do is_expected.to have_body_text /Click here to set your password/ @@ -134,7 +143,6 @@ describe Notify do end end - describe 'for users that signed up, the email' do let(:example_site_path) { root_path } let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } @@ -144,6 +152,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'a new user email', new_user_address it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' it 'should not contain the new user\'s password' do is_expected.not_to have_body_text /password/ @@ -157,6 +166,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' it 'is sent to the new user' do is_expected.to deliver_to key.user.email @@ -181,6 +191,7 @@ describe Notify do subject { Notify.new_email_email(email.id) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' it 'is sent to the new user' do is_expected.to deliver_to email.user.email @@ -227,6 +238,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ @@ -253,6 +265,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -283,6 +296,7 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -319,6 +333,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -345,6 +360,7 @@ describe Notify do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'contains the description' do is_expected.to have_body_text /#{merge_request_with_description.description}/ @@ -357,6 +373,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -387,6 +404,7 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -417,6 +435,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -446,6 +465,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /Project was moved/ @@ -468,6 +488,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /Access to project was granted/ @@ -518,6 +539,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'commit' it_behaves_like 'it should show Gmail Actions View Commit link' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -538,6 +560,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -558,6 +581,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ @@ -579,6 +603,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /Access to group was granted/ @@ -607,6 +632,7 @@ describe Notify do subject { ActionMailer::Base.deliveries.last } it_behaves_like 'an email sent from GitLab' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent to the new user' do is_expected.to deliver_to 'new-email@mail.com' @@ -629,6 +655,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -657,6 +684,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -684,6 +712,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -707,6 +736,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -734,6 +764,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -839,6 +870,7 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } it_behaves_like 'it should show Gmail Actions View Commit link' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent as the author' do sender = subject.header[:from].addrs[0] diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index e4cac105110..f6f84db57e6 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -6,7 +6,6 @@ # message :text not null # starts_at :datetime # ends_at :datetime -# alert_type :integer # created_at :datetime # updated_at :datetime # color :string(255) @@ -16,6 +15,8 @@ require 'spec_helper' describe BroadcastMessage, models: true do + include ActiveSupport::Testing::TimeHelpers + subject { create(:broadcast_message) } it { is_expected.to be_valid } @@ -35,20 +36,79 @@ describe BroadcastMessage, models: true do it { is_expected.not_to allow_value('000').for(:font) } end - describe :current do + describe '.current' do it "should return last message if time match" do - broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow) - expect(BroadcastMessage.current).to eq(broadcast_message) + message = create(:broadcast_message) + + expect(BroadcastMessage.current).to eq message end it "should return nil if time not come" do - create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) + create(:broadcast_message, :future) + expect(BroadcastMessage.current).to be_nil end it "should return nil if time has passed" do - create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) + create(:broadcast_message, :expired) + expect(BroadcastMessage.current).to be_nil end end + + describe '#active?' do + it 'is truthy when started and not ended' do + message = build(:broadcast_message) + + expect(message).to be_active + end + + it 'is falsey when ended' do + message = build(:broadcast_message, :expired) + + expect(message).not_to be_active + end + + it 'is falsey when not started' do + message = build(:broadcast_message, :future) + + expect(message).not_to be_active + end + end + + describe '#started?' do + it 'is truthy when starts_at has passed' do + message = build(:broadcast_message) + + travel_to(3.days.from_now) do + expect(message).to be_started + end + end + + it 'is falsey when starts_at is in the future' do + message = build(:broadcast_message) + + travel_to(3.days.ago) do + expect(message).not_to be_started + end + end + end + + describe '#ended?' do + it 'is truthy when ends_at has passed' do + message = build(:broadcast_message) + + travel_to(3.days.from_now) do + expect(message).to be_ended + end + end + + it 'is falsey when ends_at is in the future' do + message = build(:broadcast_message) + + travel_to(3.days.ago) do + expect(message).not_to be_ended + end + end + end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 1c22e3cb7c4..d12b9e65c82 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1,28 +1,3 @@ -# == Schema Information -# -# Table name: builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# commit_id :integer -# coverage :float -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# - require 'spec_helper' describe Ci::Build, models: true do @@ -368,21 +343,75 @@ describe Ci::Build, models: true do end end - describe :download_url do - subject { build.download_url } + describe :artifacts_download_url do + subject { build.artifacts_download_url } it "should be nil if artifact doesn't exist" do build.update_attributes(artifacts_file: nil) is_expected.to be_nil end - it 'should be nil if artifact exist' do + it 'should not be nil if artifact exist' do gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') build.update_attributes(artifacts_file: gif) is_expected.to_not be_nil end end + describe :artifacts_browse_url do + subject { build.artifacts_browse_url } + + it "should be nil if artifacts browser is unsupported" do + allow(build).to receive(:artifacts_browser_supported?).and_return(false) + is_expected.to be_nil + end + + it 'should not be nil if artifacts browser is supported' do + allow(build).to receive(:artifacts_browser_supported?).and_return(true) + is_expected.to_not be_nil + end + end + + describe :artifacts? do + subject { build.artifacts? } + + context 'artifacts archive does not exist' do + before { build.update_attributes(artifacts_file: nil) } + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + before do + gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + build.update_attributes(artifacts_file: gif) + end + + it { is_expected.to be_truthy } + end + end + + + describe :artifacts_browser_supported? do + subject { build.artifacts_browser_supported? } + context 'artifacts metadata does not exist' do + it { is_expected.to be_falsy } + end + + context 'artifacts archive is a zip file and metadata exists' do + before do + fixture_dir = Rails.root + 'spec/fixtures/' + archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip', + 'application/zip') + metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz', + 'application/x-gzip') + build.update_attributes(artifacts_file: archive) + build.update_attributes(artifacts_metadata: metadata) + end + + it { is_expected.to be_truthy } + end + end + describe :repo_url do let(:build) { FactoryGirl.create :ci_build } let(:project) { build.project } @@ -397,6 +426,30 @@ describe Ci::Build, models: true do it { is_expected.to include(project.web_url[7..-1]) } end + describe :depends_on_builds do + let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' } + let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' } + let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' } + let!(:staging) { FactoryGirl.create :ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy' } + + it 'to have no dependents if this is first build' do + expect(build.depends_on_builds).to be_empty + end + + it 'to have one dependent if this is test' do + expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) + end + + it 'to have all builds from build and test stage if this is last' do + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) + end + + it 'to have retried builds instead the original ones' do + retried_rspec = Ci::Build.retry(rspec_test) + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + end + end + def create_mr(build, commit, factory: :merge_request, created_at: Time.now) FactoryGirl.create(factory, source_project_id: commit.gl_project_id, diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb new file mode 100644 index 00000000000..5afe042e154 --- /dev/null +++ b/spec/models/identity_spec.rb @@ -0,0 +1,38 @@ +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# extern_uid :string(255) +# provider :string(255) +# user_id :integer +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +RSpec.describe Identity, models: true do + + describe 'relations' do + it { is_expected.to belong_to(:user) } + end + + describe 'fields' do + it { is_expected.to respond_to(:provider) } + it { is_expected.to respond_to(:extern_uid) } + end + + describe '#is_ldap?' do + let(:ldap_identity) { create(:identity, provider: 'ldapmain') } + let(:other_identity) { create(:identity, provider: 'twitter') } + + it 'returns true if it is a ldap identity' do + expect(ldap_identity.ldap?).to be_truthy + end + + it 'returns false if it is not a ldap identity' do + expect(other_identity.ldap?).to be_falsey + end + end +end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 151a29e974b..9182b42661d 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -178,6 +178,30 @@ describe Note, models: true do end end + describe "cross_reference_not_visible_for?" do + let(:private_user) { create(:user) } + let(:private_project) { create(:project, namespace: private_user.namespace).tap { |p| p.team << [private_user, :master] } } + let(:private_issue) { create(:issue, project: private_project) } + + let(:ext_proj) { create(:project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + + let(:note) do + create :note, + noteable: ext_issue, project: ext_proj, + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + system: true + end + + it "returns true" do + expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy + end + + it "returns false" do + expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy + end + end + describe "set_award!" do let(:issue) { create :issue } diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 876b927eaea..a2085df5bcd 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -36,6 +36,13 @@ describe ProjectWiki, models: true do end end + describe "#wiki_base_path" do + it "returns the wiki base path" do + wiki_base_path = "/#{project.path_with_namespace}/wikis" + expect(subject.wiki_base_path).to eq(wiki_base_path) + end + end + describe "#wiki" do it "contains a Gollum::Wiki instance" do expect(subject.wiki).to be_a Gollum::Wiki diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3cd63b2b0e8..0bef68e2885 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -569,27 +569,39 @@ describe User, models: true do end end - describe :ldap_user? do - it "is true if provider name starts with ldap" do - user = create(:omniauth_user, provider: 'ldapmain') - expect( user.ldap_user? ).to be_truthy - end + context 'ldap synchronized user' do + describe :ldap_user? do + it 'is true if provider name starts with ldap' do + user = create(:omniauth_user, provider: 'ldapmain') + expect(user.ldap_user?).to be_truthy + end - it "is false for other providers" do - user = create(:omniauth_user, provider: 'other-provider') - expect( user.ldap_user? ).to be_falsey + it 'is false for other providers' do + user = create(:omniauth_user, provider: 'other-provider') + expect(user.ldap_user?).to be_falsey + end + + it 'is false if no extern_uid is provided' do + user = create(:omniauth_user, extern_uid: nil) + expect(user.ldap_user?).to be_falsey + end end - it "is false if no extern_uid is provided" do - user = create(:omniauth_user, extern_uid: nil) - expect( user.ldap_user? ).to be_falsey + describe :ldap_identity do + it 'returns ldap identity' do + user = create :omniauth_user + expect(user.ldap_identity.provider).not_to be_empty + end end - end - describe :ldap_identity do - it "returns ldap identity" do - user = create :omniauth_user - expect(user.ldap_identity.provider).not_to be_empty + describe '#ldap_block' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') } + + it 'blocks user flaging the action caming from ldap' do + user.ldap_block + expect(user.blocked?).to be_truthy + expect(user.ldap_blocked?).to be_truthy + end end end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb new file mode 100644 index 00000000000..8c9f5a382b7 --- /dev/null +++ b/spec/requests/api/builds_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) } + let(:commit) { create(:ci_commit, project: project)} + let(:build) { create(:ci_build, commit: commit) } + let(:build_with_trace) { create(:ci_build_with_trace, commit: commit) } + let(:build_canceled) { create(:ci_build, :canceled, commit: commit) } + + describe 'GET /projects/:id/builds ' do + context 'authorized user' do + it 'should return project builds' do + get api("/projects/#{project.id}/builds", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + + it 'should filter project with one scope element' do + get api("/projects/#{project.id}/builds?scope=pending", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + + it 'should filter project with array of scope elements' do + get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=running", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + + it 'should respond 400 when scope contains invalid state' do + get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=unknown_status", user) + + expect(response.status).to eq(400) + end + end + + context 'unauthorized user' do + it 'should not return project builds' do + get api("/projects/#{project.id}/builds") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/repository/commits/:sha/builds' do + context 'authorized user' do + it 'should return project builds for specific commit' do + project.ensure_ci_commit(commit.sha) + get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + end + + context 'unauthorized user' do + it 'should not return project builds' do + project.ensure_ci_commit(commit.sha) + get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/builds/:build_id' do + context 'authorized user' do + it 'should return specific build data' do + get api("/projects/#{project.id}/builds/#{build.id}", user) + + expect(response.status).to eq(200) + expect(json_response['name']).to eq('test') + end + end + + context 'unauthorized user' do + it 'should not return specific build data' do + get api("/projects/#{project.id}/builds/#{build.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/builds/:build_id/trace' do + context 'authorized user' do + it 'should return specific build trace' do + get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace", user) + + expect(response.status).to eq(200) + expect(response.body).to eq(build_with_trace.trace) + end + end + + context 'unauthorized user' do + it 'should not return specific build trace' do + get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/builds/:build_id/cancel' do + context 'authorized user' do + context 'user with :manage_builds persmission' do + it 'should cancel running or pending build' do + post api("/projects/#{project.id}/builds/#{build.id}/cancel", user) + + expect(response.status).to eq(201) + expect(project.builds.first.status).to eq('canceled') + end + end + + context 'user without :manage_builds permission' do + it 'should not cancel build' do + post api("/projects/#{project.id}/builds/#{build.id}/cancel", user2) + + expect(response.status).to eq(403) + end + end + end + + context 'unauthorized user' do + it 'should not cancel build' do + post api("/projects/#{project.id}/builds/#{build.id}/cancel") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/builds/:build_id/retry' do + context 'authorized user' do + context 'user with :manage_builds persmission' do + it 'should retry non-running build' do + post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user) + + expect(response.status).to eq(201) + expect(project.builds.first.status).to eq('canceled') + expect(json_response['status']).to eq('pending') + end + end + + context 'user without :manage_builds permission' do + it 'should not retry build' do + post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user2) + + expect(response.status).to eq(403) + end + end + end + + context 'unauthorized user' do + it 'should not retry build' do + post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry") + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index a28607bd240..21482fc1070 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::CommitStatus, api: true do include ApiHelpers let(:user) { create(:user) } let(:user2) { create(:user) } @@ -12,6 +12,10 @@ describe API::API, api: true do let(:commit_status) { create(:commit_status, commit: ci_commit) } describe "GET /projects/:id/repository/commits/:sha/statuses" do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) } + end + context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 8b177af4689..39f9a06fe1b 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -10,9 +10,32 @@ describe API::API, api: true do let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } + + # For testing the cross-reference of a private issue in a public issue + let(:private_user) { create(:user) } + let(:private_project) do + create(:project, namespace: private_user.namespace). + tap { |p| p.team << [private_user, :master] } + end + let(:private_issue) { create(:issue, project: private_project) } + + let(:ext_proj) { create(:project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + + let!(:cross_reference_note) do + create :note, + noteable: ext_issue, project: ext_proj, + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + system: true + end + before { project.team << [user, :reporter] } describe "GET /projects/:id/noteable/:noteable_id/notes" do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) } + end + context "when noteable is an Issue" do it "should return an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) @@ -25,6 +48,24 @@ describe API::API, api: true do get api("/projects/#{project.id}/issues/123/notes", user) expect(response.status).to eq(404) end + + context "that references a private issue" do + it "should return an empty array" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response).to be_empty + end + + context "and current user can view the note" do + it "should return an empty array" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(cross_reference_note.note) + end + end + end end context "when noteable is a Snippet" do @@ -68,6 +109,21 @@ describe API::API, api: true do get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) expect(response.status).to eq(404) end + + context "that references a private issue" do + it "should return a 404 error" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + expect(response.status).to eq(404) + end + + context "and current user can view the note" do + it "should return an issue note by id" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + expect(response.status).to eq(200) + expect(json_response['body']).to eq(cross_reference_note.note) + end + end + end end context "when noteable is a Snippet" do diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 314bd7ddc59..2a86b60bc4d 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -3,11 +3,19 @@ require 'spec_helper' describe API::API do include ApiHelpers + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } + let!(:trigger_token_2) { 'secure_token_2' } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } + let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } + let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') } + describe 'POST /projects/:project_id/trigger' do - let!(:trigger_token) { 'secure token' } - let!(:project) { FactoryGirl.create(:project) } - let!(:project2) { FactoryGirl.create(:empty_project) } - let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } + let!(:project2) { create(:empty_project) } let(:options) do { token: trigger_token @@ -77,4 +85,127 @@ describe API::API do end end end + + describe 'GET /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'should return list of triggers' do + get api("/projects/#{project.id}/triggers", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Array) + expect(json_response[0]).to have_key('token') + end + end + + context 'authenticated user with invalid permissions' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'should return trigger details' do + get api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Hash) + end + + it 'should respond with 404 Not Found if requesting non-existing trigger' do + get api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response.status).to eq(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'should create trigger' do + expect do + post api("/projects/#{project.id}/triggers", user) + end.to change{project.triggers.count}.by(1) + + expect(response.status).to eq(201) + expect(json_response).to be_a(Hash) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not create trigger' do + post api("/projects/#{project.id}/triggers", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not create trigger' do + post api("/projects/#{project.id}/triggers") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'should delete trigger' do + expect do + delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + end.to change{project.triggers.count}.by(-1) + expect(response.status).to eq(200) + end + + it 'should respond with 404 Not Found if requesting non-existing trigger' do + delete api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response.status).to eq(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not delete trigger' do + delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not delete trigger' do + delete api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response.status).to eq(401) + end + end + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4f278551d07..b82c5c7685f 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -8,6 +8,8 @@ describe API::API, api: true do let(:key) { create(:key, user: user) } let(:email) { create(:email, user: user) } let(:omniauth_user) { create(:omniauth_user) } + let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } describe "GET /users" do context "when unauthenticated" do @@ -783,6 +785,12 @@ describe API::API, api: true do expect(user.reload.state).to eq('blocked') end + it 'should not re-block ldap blocked users' do + put api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response.status).to eq(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + it 'should not be available for non admin users' do put api("/users/#{user.id}/block", user) expect(response.status).to eq(403) @@ -797,7 +805,9 @@ describe API::API, api: true do end describe 'PUT /user/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } before { admin } + it 'should unblock existing user' do put api("/users/#{user.id}/unblock", admin) expect(response.status).to eq(200) @@ -805,12 +815,15 @@ describe API::API, api: true do end it 'should unblock a blocked user' do - put api("/users/#{user.id}/block", admin) - expect(response.status).to eq(200) - expect(user.reload.state).to eq('blocked') - put api("/users/#{user.id}/unblock", admin) + put api("/users/#{blocked_user.id}/unblock", admin) expect(response.status).to eq(200) - expect(user.reload.state).to eq('active') + expect(blocked_user.reload.state).to eq('active') + end + + it 'should not unblock ldap blocked users' do + put api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response.status).to eq(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'should not be available for non admin users' do diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb new file mode 100644 index 00000000000..9744729ba0c --- /dev/null +++ b/spec/requests/api/variables_spec.rb @@ -0,0 +1,182 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } + let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:variable) { create(:ci_variable, project: project) } + + describe 'GET /projects/:id/variables' do + context 'authorized user with proper permissions' do + it 'should return project variables' do + get api("/projects/#{project.id}/variables", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Array) + end + end + + context 'authorized user with invalid permissions' do + it 'should not return project variables' do + get api("/projects/#{project.id}/variables", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not return project variables' do + get api("/projects/#{project.id}/variables") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}", user) + + expect(response.status).to eq(200) + expect(json_response['value']).to eq(variable.value) + end + + it 'should respond with 404 Not Found if requesting non-existing variable' do + get api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/variables' do + context 'authorized user with proper permissions' do + it 'should create variable' do + expect do + post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' + end.to change{project.variables.count}.by(1) + + expect(response.status).to eq(201) + expect(json_response['key']).to eq('TEST_VARIABLE_2') + expect(json_response['value']).to eq('VALUE_2') + end + + it 'should not allow to duplicate variable key' do + expect do + post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2' + end.to change{project.variables.count}.by(0) + + expect(response.status).to eq(400) + end + end + + context 'authorized user with invalid permissions' do + it 'should not create variable' do + post api("/projects/#{project.id}/variables", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not create variable' do + post api("/projects/#{project.id}/variables") + + expect(response.status).to eq(401) + end + end + end + + describe 'PUT /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should update variable data' do + initial_variable = project.variables.first + value_before = initial_variable.value + + put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP' + + updated_variable = project.variables.first + + expect(response.status).to eq(200) + expect(value_before).to eq(variable.value) + expect(updated_variable.value).to eq('VALUE_1_UP') + end + + it 'should responde with 404 Not Found if requesting non-existing variable' do + put api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not update variable' do + put api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not update variable' do + put api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should delete variable' do + expect do + delete api("/projects/#{project.id}/variables/#{variable.key}", user) + end.to change{project.variables.count}.by(-1) + expect(response.status).to eq(200) + end + + it 'should responde with 404 Not Found if requesting non-existing variable' do + delete api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not delete variable' do + delete api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not delete variable' do + delete api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c27e87c4acc..eec927102aa 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -101,6 +101,18 @@ describe Ci::API::API do { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false }, ]) end + + it "returns dependent builds" do + commit = FactoryGirl.create(:ci_commit, project: project) + commit.create_builds('master', false, nil, nil) + commit.builds.where(stage: 'test').each(&:success) + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response["depends_on_builds"].count).to eq(2) + expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec") + end end describe "PUT /builds/:id" do @@ -210,6 +222,52 @@ describe Ci::API::API do end end + context 'should post artifacts file and metadata file' do + let!(:artifacts) { file_upload } + let!(:metadata) { file_upload2 } + + let(:stored_artifacts_file) { build.reload.artifacts_file.file } + let(:stored_metadata_file) { build.reload.artifacts_metadata.file } + + before do + build.run! + post(post_url, post_data, headers_with_token) + end + + context 'post data accelerated by workhorse is correct' do + let(:post_data) do + { 'file.path' => artifacts.path, + 'file.name' => artifacts.original_filename, + 'metadata.path' => metadata.path, + 'metadata.name' => metadata.original_filename } + end + + it 'responds with valid status' do + expect(response.status).to eq(201) + end + + it 'stores artifacts and artifacts metadata' do + expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) + expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) + end + end + + context 'no artifacts file in post data' do + let(:post_data) do + { 'metadata' => metadata } + end + + it 'is expected to respond with bad request' do + expect(response.status).to eq(400) + end + + it 'does not store metadata' do + expect(stored_metadata_file).to be_nil + end + end + end + + context "should fail to post too large artifact" do before do build.run! diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb new file mode 100644 index 00000000000..ce7d1455975 --- /dev/null +++ b/spec/services/repair_ldap_blocked_user_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe RepairLdapBlockedUserService, services: true do + let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + let(:identity) { user.ldap_identity } + subject(:service) { RepairLdapBlockedUserService.new(user) } + + describe '#execute' do + it 'change to normal block after destroying last ldap identity' do + identity.destroy + service.execute + + expect(user.reload).not_to be_ldap_blocked + end + + it 'change to normal block after changing last ldap identity to another provider' do + identity.update_attribute(:provider, 'twitter') + service.execute + + expect(user.reload).not_to be_ldap_blocked + end + end +end diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb new file mode 100644 index 00000000000..352a6eeec79 --- /dev/null +++ b/spec/support/api/pagination_shared_examples.rb @@ -0,0 +1,20 @@ +# Specs for paginated resources. +# +# Requires an API request: +# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } +shared_examples 'a paginated resources' do + before do + # Fires the request + request + end + + it 'has pagination headers' do + expect(response.headers).to include('X-Total') + expect(response.headers).to include('X-Total-Pages') + expect(response.headers).to include('X-Per-Page') + expect(response.headers).to include('X-Page') + expect(response.headers).to include('X-Next-Page') + expect(response.headers).to include('X-Prev-Page') + expect(response.headers).to include('Link') + end +end diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index 3482145404e..a5b256bd3ec 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -36,8 +36,8 @@ staging: script: "cap deploy stating" type: deploy tags: - - capistrano - - debian + - ruby + - mysql except: - stable @@ -47,8 +47,8 @@ production: - cap deploy production - cap notify tags: - - capistrano - - debian + - ruby + - mysql only: - master - /^deploy-.*$/ diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 5d97fdd4882..73c6792b65f 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -28,6 +28,10 @@ class MarkdownFeature end end + def project_wiki + @project_wiki ||= ProjectWiki.new(project, user) + end + def issue @issue ||= create(:issue, project: project) end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index b251e7f8f23..1d52489e804 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -66,6 +66,24 @@ module MarkdownMatchers end end + # GollumTagsFilter + matcher :parse_gollum_tags do + def have_image(src) + have_css("img[src$='#{src}']") + end + + 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('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('http://example.com/images/example.jpg') + end + end + # UserReferenceFilter matcher :reference_users do set_default_markdown_messages diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js new file mode 100755 index 00000000000..cfa49e72c50 --- /dev/null +++ b/vendor/assets/javascripts/autosize.js @@ -0,0 +1,243 @@ +/*! + Autosize 3.0.14 + license: MIT + http://www.jacklmoore.com/autosize +*/ +(function (global, factory) { + if (typeof define === 'function' && define.amd) { + define(['exports', 'module'], factory); + } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { + factory(exports, module); + } else { + var mod = { + exports: {} + }; + factory(mod.exports, mod); + global.autosize = mod.exports; + } +})(this, function (exports, module) { + 'use strict'; + + var set = typeof Set === 'function' ? new Set() : (function () { + var list = []; + + return { + has: function has(key) { + return Boolean(list.indexOf(key) > -1); + }, + add: function add(key) { + list.push(key); + }, + 'delete': function _delete(key) { + list.splice(list.indexOf(key), 1); + } }; + })(); + + function assign(ta) { + var _ref = arguments[1] === undefined ? {} : arguments[1]; + + var _ref$setOverflowX = _ref.setOverflowX; + var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX; + var _ref$setOverflowY = _ref.setOverflowY; + var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY; + + if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return; + + var heightOffset = null; + var overflowY = null; + var clientWidth = ta.clientWidth; + + function init() { + var style = window.getComputedStyle(ta, null); + + overflowY = style.overflowY; + + if (style.resize === 'vertical') { + ta.style.resize = 'none'; + } else if (style.resize === 'both') { + ta.style.resize = 'horizontal'; + } + + if (style.boxSizing === 'content-box') { + heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)); + } else { + heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth); + } + // Fix when a textarea is not on document body and heightOffset is Not a Number + if (isNaN(heightOffset)) { + heightOffset = 0; + } + + update(); + } + + function changeOverflow(value) { + { + // Chrome/Safari-specific fix: + // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space + // made available by removing the scrollbar. The following forces the necessary text reflow. + var width = ta.style.width; + ta.style.width = '0px'; + // Force reflow: + /* jshint ignore:start */ + ta.offsetWidth; + /* jshint ignore:end */ + ta.style.width = width; + } + + overflowY = value; + + if (setOverflowY) { + ta.style.overflowY = value; + } + + resize(); + } + + function resize() { + var htmlTop = window.pageYOffset; + var bodyTop = document.body.scrollTop; + var originalHeight = ta.style.height; + + ta.style.height = 'auto'; + + var endHeight = ta.scrollHeight + heightOffset; + + if (ta.scrollHeight === 0) { + // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM. + ta.style.height = originalHeight; + return; + } + + ta.style.height = endHeight + 'px'; + + // used to check if an update is actually necessary on window.resize + clientWidth = ta.clientWidth; + + // prevents scroll-position jumping + document.documentElement.scrollTop = htmlTop; + document.body.scrollTop = bodyTop; + } + + function update() { + var startHeight = ta.style.height; + + resize(); + + var style = window.getComputedStyle(ta, null); + + if (style.height !== ta.style.height) { + if (overflowY !== 'visible') { + changeOverflow('visible'); + } + } else { + if (overflowY !== 'hidden') { + changeOverflow('hidden'); + } + } + + if (startHeight !== ta.style.height) { + var evt = document.createEvent('Event'); + evt.initEvent('autosize:resized', true, false); + ta.dispatchEvent(evt); + } + } + + var pageResize = function pageResize() { + if (ta.clientWidth !== clientWidth) { + update(); + } + }; + + var destroy = (function (style) { + window.removeEventListener('resize', pageResize, false); + ta.removeEventListener('input', update, false); + ta.removeEventListener('keyup', update, false); + ta.removeEventListener('autosize:destroy', destroy, false); + ta.removeEventListener('autosize:update', update, false); + set['delete'](ta); + + Object.keys(style).forEach(function (key) { + ta.style[key] = style[key]; + }); + }).bind(ta, { + height: ta.style.height, + resize: ta.style.resize, + overflowY: ta.style.overflowY, + overflowX: ta.style.overflowX, + wordWrap: ta.style.wordWrap }); + + ta.addEventListener('autosize:destroy', destroy, false); + + // IE9 does not fire onpropertychange or oninput for deletions, + // so binding to onkeyup to catch most of those events. + // There is no way that I know of to detect something like 'cut' in IE9. + if ('onpropertychange' in ta && 'oninput' in ta) { + ta.addEventListener('keyup', update, false); + } + + window.addEventListener('resize', pageResize, false); + ta.addEventListener('input', update, false); + ta.addEventListener('autosize:update', update, false); + set.add(ta); + + if (setOverflowX) { + ta.style.overflowX = 'hidden'; + ta.style.wordWrap = 'break-word'; + } + + init(); + } + + function destroy(ta) { + if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return; + var evt = document.createEvent('Event'); + evt.initEvent('autosize:destroy', true, false); + ta.dispatchEvent(evt); + } + + function update(ta) { + if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return; + var evt = document.createEvent('Event'); + evt.initEvent('autosize:update', true, false); + ta.dispatchEvent(evt); + } + + var autosize = null; + + // Do nothing in Node.js environment and IE8 (or lower) + if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') { + autosize = function (el) { + return el; + }; + autosize.destroy = function (el) { + return el; + }; + autosize.update = function (el) { + return el; + }; + } else { + autosize = function (el, options) { + if (el) { + Array.prototype.forEach.call(el.length ? el : [el], function (x) { + return assign(x, options); + }); + } + return el; + }; + autosize.destroy = function (el) { + if (el) { + Array.prototype.forEach.call(el.length ? el : [el], destroy); + } + return el; + }; + autosize.update = function (el) { + if (el) { + Array.prototype.forEach.call(el.length ? el : [el], update); + } + return el; + }; + } + + module.exports = autosize; +});
\ No newline at end of file diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js new file mode 100644 index 00000000000..da37966b28a --- /dev/null +++ b/vendor/assets/javascripts/latinise.js @@ -0,0 +1,11 @@ +// Converting text to basic latin (aka removing accents) +// +// Based on: http://semplicewebsites.com/removing-accents-javascript +// +var Latinise = { + map: {"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"} +}; + +String.prototype.latinise = function() { + return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; }); +}; |