diff options
193 files changed, 2370 insertions, 921 deletions
diff --git a/CHANGELOG b/CHANGELOG index 01e7e7a1606..0c3aadc29d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) + - Fix builds API response not including commit data - Fix error when CI job variables key specified but not defined - Fix pipeline status when there are no builds in pipeline - Fix Error 500 when using closes_issues API with an external issue tracker @@ -8,6 +9,7 @@ v 8.9.0 (unreleased) - Bulk assign/unassign labels to issues. - Ability to prioritize labels !4009 / !3205 (Thijs Wouters) - Show Star and Fork buttons on mobile. + - Performance improvements on RelativeLinkFilter - Fix endless redirections when accessing user OAuth applications when they are disabled - Allow enabling wiki page events from Webhook management UI - Bump rouge to 1.11.0 @@ -18,7 +20,9 @@ v 8.9.0 (unreleased) - Redesign all Devise emails. !4297 - Don't show 'Leave Project' to group members - Fix wiki page events' webhook to point to the wiki repository + - Add a border around images to differentiate them from the background. - Don't show tags for revert and cherry-pick operations + - Show image ID on registry page - Fix issue todo not remove when leave project !4150 (Long Nguyen) - Allow customisable text on the 'nearly there' page after a user signs up - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support @@ -70,6 +74,7 @@ v 8.9.0 (unreleased) - Todos will display target state if issuable target is 'Closed' or 'Merged' - Validate only and except regexp - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project - Add support for using Yubikeys (U2F) for two-factor authentication - Link to blank group icon doesn't throw a 404 anymore - Remove 'main language' feature @@ -77,13 +82,16 @@ v 8.9.0 (unreleased) - Pipelines can be canceled only when there are running builds - Allow authentication using personal access tokens - Use downcased path to container repository as this is expected path by Docker + - Allow to use CI token to fetch LFS objects - Custom notification settings - Projects pending deletion will render a 404 page - Measure queue duration between gitlab-workhorse and Rails - Added Gfm autocomplete for labels + - Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114 - Make Omniauth providers specs to not modify global configuration - Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir) - Make authentication service for Container Registry to be compatible with < Docker 1.11 + - Make it possible to lock a runner from being enabled for other projects - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav - Use Knapsack only in CI environment @@ -101,6 +109,7 @@ v 8.9.0 (unreleased) - An indicator is now displayed at the top of the comment field for confidential issues. - Show categorised search queries in the search autocomplete - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented + - Dropdown for `.gitlab-ci.yml` templates - Improve issuables APIs performance when accessing notes !4471 - Add sorting dropdown to tags page !4423 - External links now open in a new tab @@ -129,11 +138,16 @@ v 8.9.0 (unreleased) - Various associations are now eager loaded when parsing issue references to reduce the number of queries executed - Set inverse_of for Project/Service association to reduce the number of queries - Update tanuki logo highlight/loading colors + - Remove explicit Gitlab::Metrics.action assignments, are already automatic. - Use Git cached counters for branches and tags on project page + - Cache participable participants in an instance variable. - Filter parameters for request_uri value on instrumented transactions. - Remove duplicated keys add UNIQUE index to keys fingerprint column + - ExtractsPath get ref_names from repository cache, if not there access git. - Cache user todo counts from TodoService - Ensure Todos counters doesn't count Todos for projects pending delete + - Add left/right arrows horizontal navigation + - Add tooltip to pin/unpin navbar v 8.8.5 - Import GitHub repositories respecting the API rate limit !4166 diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 3f61ea1eaf4..cf46f15a156 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -7,6 +7,7 @@ labelsPath: "/api/:version/projects/:id/labels" licensePath: "/api/:version/licenses/:key" gitignorePath: "/api/:version/gitignores/:key" + gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key" group: (group_id, callback) -> url = Api.buildUrl(Api.groupPath) @@ -110,6 +111,12 @@ $.get url, (gitignore) -> callback(gitignore) + gitlabCiYml: (key, callback) -> + url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key) + + $.get url, (file) -> + callback(file) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 2f9f6c3ef5b..397892d15c0 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -257,7 +257,7 @@ $ -> # Sidenav pinning if $(window).width() < 1440 and $.cookie('pin_nav') is 'true' - $.cookie('pin_nav', 'false') + $.cookie('pin_nav', 'false', { path: '/' }) $('.page-with-sidebar') .toggleClass('page-sidebar-collapsed page-sidebar-expanded') .removeClass('page-sidebar-pinned') @@ -268,17 +268,33 @@ $ -> .on 'click', '.js-nav-pin', (e) -> e.preventDefault() + $pinBtn = $(e.currentTarget) + $page = $ '.page-with-sidebar' + $topNav = $ '.navbar-fixed-top' + $tooltip = $ "##{$pinBtn.attr('aria-describedby')}" + doPinNav = not $page.is('.page-sidebar-pinned') + tooltipText = 'Pin navigation' + $(this).toggleClass 'is-active' - if $.cookie('pin_nav') is 'true' - $.cookie 'pin_nav', 'false' - $('.page-with-sidebar') - .removeClass('page-sidebar-pinned') - .toggleClass('page-sidebar-collapsed page-sidebar-expanded') - $('.navbar-fixed-top') - .removeClass('header-pinned-nav') - .toggleClass('header-collapsed header-expanded') + if doPinNav + $page.addClass('page-sidebar-pinned') + $topNav.addClass('header-pinned-nav') else - $.cookie 'pin_nav', 'true' - $('.page-with-sidebar').addClass('page-sidebar-pinned') - $('.navbar-fixed-top').addClass('header-pinned-nav') + $tooltip.remove() # Remove it immediately when collapsing the sidebar + $page.removeClass('page-sidebar-pinned') + .toggleClass('page-sidebar-collapsed page-sidebar-expanded') + $topNav.removeClass('header-pinned-nav') + .toggleClass('header-collapsed header-expanded') + + # Save settings + $.cookie 'pin_nav', doPinNav, { path: '/' } + + if $.cookie('pin_nav') is 'true' or doPinNav + tooltipText = 'Unpin navigation' + + # Update tooltip text immediately + $tooltip.find('.tooltip-inner').text(tooltipText) + + # Persist tooltip title + $pinBtn.attr('title', tooltipText).tooltip('fixTitle') diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee new file mode 100644 index 00000000000..d9a03d05529 --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee @@ -0,0 +1,23 @@ +#= require blob/template_selector + +class @BlobCiYamlSelector extends TemplateSelector + requestFile: (query) -> + Api.gitlabCiYml query.name, @requestFileSuccess.bind(@) + +class @BlobCiYamlSelectors + constructor: (opts) -> + { + @$dropdowns = $('.js-gitlab-ci-yml-selector') + @editor + } = opts + + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) + + new BlobCiYamlSelector( + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown, + editor: @editor + ) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 636f909dbd0..19e584519d7 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -15,6 +15,7 @@ class @EditBlob new BlobLicenseSelectors { @editor } new BlobGitignoreSelectors { @editor } + new BlobCiYamlSelectors { @editor } initModePanesAndLinks: -> @$editModePanes = $(".js-edit-mode-pane") diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 2a8a1f05b35..2a7bf0bc306 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -58,7 +58,7 @@ class GitLabDropdownFilter filter: (search_text) -> data = @options.data() - if data? + if data? and not @options.filterByText results = data if search_text isnt '' @@ -102,10 +102,11 @@ class GitLabDropdownFilter $el = $(@) matches = fuzzaldrinPlus.match($el.text().trim(), search_text) - if matches.length - $el.show() - else - $el.hide() + unless $el.is('.dropdown-header') + if matches.length + $el.show() + else + $el.hide() else elements.show() @@ -191,6 +192,7 @@ class GitLabDropdown if @options.filterable @filter = new GitLabDropdownFilter @filterInput, filterInputBlur: @filterInputBlur + filterByText: @options.filterByText remote: @options.filterRemote query: @options.data keys: searchFields diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee index d540cc4dc46..77512d187c9 100644 --- a/app/assets/javascripts/gl_form.js.coffee +++ b/app/assets/javascripts/gl_form.js.coffee @@ -34,6 +34,8 @@ class @GLForm # form and textarea event listeners @addEventListeners() + gl.text.init(@form) + # hide discard button @form.find('.js-note-discard').hide() @@ -42,6 +44,7 @@ class @GLForm clearEventListeners: -> @textarea.off 'focus' @textarea.off 'blur' + gl.text.removeListeners(@form) addEventListeners: -> @textarea.on 'focus', -> diff --git a/app/assets/javascripts/lib/text_utility.js.coffee b/app/assets/javascripts/lib/text_utility.js.coffee new file mode 100644 index 00000000000..bb2772dfed2 --- /dev/null +++ b/app/assets/javascripts/lib/text_utility.js.coffee @@ -0,0 +1,79 @@ +((w) -> + w.gl ?= {} + w.gl.text ?= {} + + gl.text.randomString = -> Math.random().toString(36).substring(7) + + gl.text.replaceRange = (s, start, end, substitute) -> + s.substring(0, start) + substitute + s.substring(end); + + gl.text.selectedText = (text, textarea) -> + text.substring(textarea.selectionStart, textarea.selectionEnd) + + gl.text.insertText = (textArea, text, tag, selected, wrap) -> + selectedSplit = selected.split('\n') + startChar = if not wrap and textArea.selectionStart > 0 then '\n' else '' + + if selectedSplit.length > 1 and not wrap + insertText = selectedSplit.map((val) -> + if val.indexOf(tag) is 0 + "#{val.replace(tag, '')}" + else + "#{tag}#{val}" + ).join('\n') + else + insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}" + + if document.queryCommandSupported('insertText') + document.execCommand 'insertText', false, insertText + else + try + document.execCommand("ms-beginUndoUnit") + + textArea.value = @replaceRange( + text, + textArea.selectionStart, + textArea.selectionEnd, + insertText) + try + document.execCommand("ms-endUndoUnit") + + @moveCursor(textArea, tag, wrap) + + gl.text.moveCursor = (textArea, tag, wrapped) -> + return unless textArea.setSelectionRange + + if textArea.selectionStart is textArea.selectionEnd + if wrapped + pos = textArea.selectionStart - tag.length + else + pos = textArea.selectionStart + + textArea.setSelectionRange pos, pos + + gl.text.updateText = (textArea, tag, wrap) -> + $textArea = $(textArea) + oldVal = $textArea.val() + textArea = $textArea.get(0) + text = $textArea.val() + selected = @selectedText(text, textArea) + $textArea.focus() + + @insertText(textArea, text, tag, selected, wrap) + + gl.text.init = (form) -> + self = @ + $('.js-md', form) + .off 'click' + .on 'click', -> + $this = $(@) + self.updateText( + $this.closest('.md-area').find('textarea'), + $this.data('md-tag'), + not $this.data('md-prepend') + ) + + gl.text.removeListeners = (form) -> + $('.js-md', form).off() + +) window diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index e2d3241437b..17f7e180127 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -102,12 +102,15 @@ class @Notes keydownNoteText: (e) -> $this = $(this) - if $this.val() is '' and e.which is 38 #aka the up key + if $this.val() is '' and e.which is 38 and not isMetaKey e myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last") if myLastNote.length myLastNoteEditBtn = myLastNote.find('.js-note-edit') myLastNoteEditBtn.trigger('click', [true, myLastNote]) + isMetaKey = (e) -> + (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey) + initRefresh: -> clearInterval(Notes.interval) Notes.interval = setInterval => diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee index 74d2298c1fa..0bbd082c156 100644 --- a/app/assets/javascripts/notifications_dropdown.js.coffee +++ b/app/assets/javascripts/notifications_dropdown.js.coffee @@ -1,5 +1,5 @@ class @NotificationsDropdown - $ -> + constructor: -> $(document) .off 'click', '.update-notification' .on 'click', '.update-notification', (e) -> @@ -18,7 +18,8 @@ class @NotificationsDropdown .off 'ajax:success', '.notification-form' .on 'ajax:success', '.notification-form', (e, data) -> if data.saved - new Flash('Notification settings saved', 'notice') - $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html) + $(e.currentTarget) + .closest('.notification-dropdown') + .replaceWith(data.html) else new Flash('Failed to save new settings', 'alert') diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index d12bad97a05..96e10dd7e8a 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -19,6 +19,7 @@ class @Project $('.clone').text(url) # Ref switcher + @initRefSwitcher() $('.project-refs-select').on 'change', -> $(@).parents('form').submit() @@ -34,7 +35,6 @@ class @Project $(@).parents('.no-password-message').remove() e.preventDefault() - @projectSelectDropdown() projectSelectDropdown: -> @@ -50,3 +50,39 @@ class @Project changeProject: (url) -> window.location = url + + initRefSwitcher: -> + $('.js-project-refs-dropdown').each -> + $dropdown = $(@) + selected = $dropdown.data('selected') + + $dropdown.glDropdown( + data: (term, callback) -> + $.ajax( + url: $dropdown.data('refs-url') + data: + ref: $dropdown.data('ref') + ).done (refs) -> + callback(refs) + selectable: true + filterable: true + filterByText: true + fieldName: 'ref' + renderRow: (ref) -> + if ref.header? + "<li class='dropdown-header'>#{ref.header}</li>" + else + isActiveClass = if ref is selected then 'is-active' else '' + + "<li> + <a href='#' data-ref='#{escape(ref)}' class='#{isActiveClass}'> + #{ref} + </a> + </li>" + id: (obj, $el) -> + $el.data('ref') + toggleLabel: (obj, $el) -> + $el.text().trim() + clicked: (e) -> + $dropdown.closest('form').submit() + ) diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 3cbddc59f11..a306b8f3f29 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -37,3 +37,4 @@ @import "framework/timeline.scss"; @import "framework/typography.scss"; @import "framework/zen.scss"; +@import "framework/blank"; diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss new file mode 100644 index 00000000000..40b5171a8c6 --- /dev/null +++ b/app/assets/stylesheets/framework/blank.scss @@ -0,0 +1,23 @@ +.blank-state { + padding-top: 20px; + padding-bottom: 20px; + text-align: center; +} + +.blank-state-no-icon { + padding-top: 40px; + padding-bottom: 40px; +} + +.blank-state-title { + margin-top: 0; + margin-bottom: 5px; + font-size: 19px; + font-weight: normal; +} + +.blank-state-text { + margin-top: 0; + margin-bottom: $gl-padding; + font-size: 15px; +} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index fd885b38680..fd8eaa8a691 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -65,6 +65,11 @@ a { padding-top: 0; line-height: 1; + border-bottom: 1px solid $border-color; + + &.btn.btn-xs { + padding: 2px 5px; + } } } } @@ -97,5 +102,30 @@ white-space: pre-wrap; word-break: keep-all; } + + @include bulleted-list; + } +} + +.toolbar-group { + float: left; + margin-right: -5px; + margin-left: $gl-padding; + + &:first-child { + margin-left: 0; + } +} + +.toolbar-btn { + float: left; + padding: 0 5px; + color: #959494; + background: transparent; + border: 0; + outline: 0; + + &:hover { + color: $gl-link-color; } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 828e7224231..5ec5a96a597 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -110,3 +110,17 @@ font-size: 16px; line-height: 24px; } + +@mixin bulleted-list { + > ul { + list-style-type: disc; + + ul { + list-style-type: circle; + + ul { + list-style-type: square; + } + } + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index a55918f8711..694f09c0464 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -18,6 +18,13 @@ opacity: 0; transition-duration: .3s; } + + .fa { + position: relative; + top: 3px; + font-size: 13px; + color: $btn-placeholder-gray; + } } @mixin scrolling-links() { @@ -136,7 +143,7 @@ } /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-max) { + @media (max-width: $screen-xs-max) { width: 100%; } } @@ -220,6 +227,7 @@ form { display: block; height: auto; + margin-bottom: 14px; input { width: 100%; @@ -319,11 +327,19 @@ .fade-right { @include fade(left, rgba(250, 250, 250, 0.4), $background-color); right: 0; + + .fa { + right: -7px; + } } .fade-left { @include fade(right, rgba(250, 250, 250, 0.4), $background-color); left: 0; + + .fa { + left: -7px; + } } li { diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index ae7bdf14c40..874416e1007 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -9,6 +9,10 @@ margin-top: -2px; float: right; } + + .dropdown-menu-toggle { + line-height: 20px; + } } .panel-body { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index f242706ebe4..21d87cc9d34 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -165,11 +165,6 @@ background-size: 16px 16px !important; } -/** Branch/tag selector **/ -.project-refs-form .select2-container { - width: 160px !important; -} - .select2-results .select2-no-results, .select2-results .select2-searching, .select2-results .select2-ajax-error, diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 761e33f0df7..de534d28421 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -80,9 +80,14 @@ .commit { padding: 10px 0; + position: relative; @media (min-width: $screen-sm-min) { - padding-left: 46px; + padding-left: 20px; + + .commit-info-block { + padding-left: 44px; + } } &:not(:last-child) { @@ -95,8 +100,11 @@ vertical-align: baseline; } + .avatar { - margin-left: -46px; + position: absolute; + top: 10px; + left: 16px; } .item-title { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 1a7d5f9666e..5286b73cc50 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -4,6 +4,11 @@ margin-bottom: $gl-padding; border-radius: 3px; + .commit-short-id { + font-family: $regular_font; + font-weight: 400; + } + .diff-header { position: relative; background: $background-color; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index a34b06f1054..1aa4e06d975 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -60,13 +60,14 @@ .encoding-selector, .license-selector, - .gitignore-selector { + .gitignore-selector, + .gitlab-ci-yml-selector { display: inline-block; vertical-align: top; font-family: $regular_font; } - .gitignore-selector, .license-selector { + .gitignore-selector, .license-selector, .gitlab-ci-yml-selector { .dropdown { line-height: 21px; } @@ -76,4 +77,10 @@ width: 220px; } } + + .gitlab-ci-yml-selector { + .dropdown-menu-toggle { + width: 250px; + } + } } diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 4a95b7b852e..0b710ef168b 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -57,4 +57,11 @@ .documentation { padding: 7px; + + // Border around images in the help pages. + img:not(.emoji) { + border: 1px solid $table-border-gray; + padding: 5px; + margin: 5px; + } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 687117233f6..21ff6ab71f0 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -4,6 +4,13 @@ margin-right: 1px; } } + + // Border around images in issue and MR descriptions. + .description img:not(.emoji) { + border: 1px solid $table-border-gray; + padding: 5px; + margin: 5px; + } } .issuable-filter-count { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index e67271adfb1..aca82f7f7bf 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -119,7 +119,12 @@ margin-bottom: 0; } - @media (max-width: $screen-sm-max) { + .btn-grouped { + margin-left: 0; + margin-right: 7px; + } + + @media (max-width: $screen-xs-max) { h4 { font-size: 15px; } @@ -131,10 +136,14 @@ .btn, .btn-group, .accept-action { - width: 100%; margin-bottom: 4px; } + .accept-action { + width: 100%; + text-align: center; + } + .accept-control { width: 100%; text-align: center; @@ -284,7 +293,7 @@ margin-bottom: 0; } - @media (min-width: $screen-sm-min) { + @media (min-width: $screen-xs-min) { float: left; width: 50%; margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 577dddae741..3784010348a 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -179,6 +179,10 @@ border-top: 1px solid $border-color; } +.md-helper { + padding-top: 10px; +} + .toolbar-button { padding: 0; background: none; @@ -219,3 +223,16 @@ float: left; } } + +.note-form-actions { + @media (max-width: $screen-xs-max) { + .btn { + float: none; + width: 100%; + + &:not(:last-child) { + margin-bottom: 10px; + } + } + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 35d728aec83..ffba3dc5bc6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -84,24 +84,14 @@ ul.notes { word-wrap: break-word; @include md-typography; + // Reset ul style types since we're nested inside a ul already + @include bulleted-list; + // On diffs code should wrap nicely and not overflow code { white-space: pre-wrap; } - // Reset ul style types since we're nested inside a ul already - & > ul { - list-style-type: disc; - - ul { - list-style-type: circle; - - ul { - list-style-type: square; - } - } - } - ul.task-list { ul:not(.task-list) { padding-left: 1.3em; @@ -117,6 +107,13 @@ ul.notes { code { word-break: keep-all; } + + // Border around images in issue and MR comments. + img:not(.emoji) { + border: 1px solid $table-border-gray; + padding: 5px; + margin: 5px 0; + } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f138a2f5387..d3e59d7fdb9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -101,7 +101,8 @@ .notifications-btn { - .fa-bell { + .fa-bell, + .fa-spinner { margin-right: 6px; } @@ -373,7 +374,7 @@ a.deploy-project-label { .project-stats { margin-top: $gl-padding; margin-bottom: 0; - padding: 16px 0; + padding: 0; background-color: $white-light; font-size: 0; @@ -382,13 +383,14 @@ a.deploy-project-label { } .nav li { - display: inline; + display: inline-block; + margin: 16px 0; + margin-right: 16px; } .nav > li > a { background-color: transparent; - margin-right: 12px; - padding: 0 10px; + padding: 5px 10px; font-size: 15px; color: $notes-light-color; } @@ -402,12 +404,17 @@ a.deploy-project-label { font-size: 17px; } - li.missing a { - color: #5a6069; - border: 1px dashed #dce0e5; + li.missing { + border: 1px dashed $border-gray-light; + border-radius: $border-radius-default; + + a { + color: $notes-light-color; + display: block; + } &:hover { - background-color: #f0f2f5; + background-color: $gray-normal; } } @@ -616,3 +623,9 @@ pre.light-well { color: $gl-text-green; } } + +.project-refs-form { + .dropdown-menu { + width: 300px; + } +} diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index d25619d94e0..bf20c5305a7 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -1,15 +1,14 @@ class Admin::RunnerProjectsController < Admin::ApplicationController before_action :project, only: [:create] - def index - @runner_projects = project.runner_projects.all - @runner_project = project.runner_projects.new - end - def create @runner = Ci::Runner.find(params[:runner_project][:runner_id]) - if @runner.assign_to(@project, current_user) + return head(403) if @runner.is_shared? || @runner.locked? + + runner_project = @runner.assign_to(@project, current_user) + + if runner_project.persisted? redirect_to admin_runner_path(@runner) else redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project' diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 776ba92c9ab..996909a28c6 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -74,7 +74,7 @@ class Projects::ApplicationController < ApplicationController end def require_branch_head - unless @repository.branch_names.include?(@ref) + unless @repository.branch_exists?(@ref) redirect_to( namespace_project_tree_path(@project.namespace, @project, @ref), notice: "This action is not allowed unless you are on a branch" diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 127bd1a4318..487963fdcd7 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -54,6 +54,6 @@ class Projects::PipelinesController < Projects::ApplicationController end def commit - @commit ||= @pipeline.commit_data + @commit ||= @pipeline.commit end end diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index bedeb4a295c..dc1a18f8d42 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController def create @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + return head(403) if @runner.is_shared? || @runner.locked? return head(403) unless current_user.ci_authorized_runners.include?(@runner) path = runners_path(project) + runner_project = @runner.assign_to(project, current_user) - if @runner.assign_to(project, current_user) + if runner_project.persisted? redirect_to path else redirect_to path, alert: 'Failed adding runner to project' diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 0b4fa572501..53c36635efe 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController layout 'project_settings' def index - @runners = project.runners.ordered - @specific_runners = current_user.ci_authorized_runners. - where.not(id: project.runners). - ordered.page(params[:page]).per(20) + @project_runners = project.runners.ordered + @assignable_runners = current_user.ci_authorized_runners. + assignable_for(project).ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8044c637825..2b1f50fd01e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,7 @@ class ProjectsController < Projects::ApplicationController include ExtractsPath - before_action :authenticate_user!, except: [:show, :activity] + before_action :authenticate_user!, except: [:show, :activity, :refs] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? @@ -251,6 +251,24 @@ class ProjectsController < Projects::ApplicationController } end + def refs + options = { + 'Branches' => @repository.branch_names, + } + + unless @repository.tag_count.zero? + options['Tags'] = VersionSorter.rsort(@repository.tag_names) + end + + # If reference is commit id - we should add it to branch/tag selectbox + ref = Addressable::URI.unescape(params[:ref]) + if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/ + options['Commits'] = [ref] + end + + render json: options.to_json + end + private def determine_layout @@ -285,8 +303,14 @@ class ProjectsController < Projects::ApplicationController project.repository_exists? && !project.empty_repo? end - # Override get_id from ExtractsPath, which returns the branch and file path + # Override extract_ref from ExtractsPath, which returns the branch and file path # for the blob/tree, which in this case is just the root of the default branch. + # This way we avoid to access the repository.ref_names. + def extract_ref(_id) + [get_id, ''] + end + + # Override get_id from ExtractsPath in this case is just the root of the default branch. def get_id project.repository.root_ref end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 439b015b3b8..41859841834 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -101,22 +101,6 @@ module ApplicationHelper 'Never' end - def grouped_options_refs - repository = @project.repository - - options = [ - ['Branches', repository.branch_names], - ['Tags', VersionSorter.rsort(repository.tag_names)] - ] - - # If reference is commit id - we should add it to branch/tag selectbox - if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/ - options << ['Commit', [@ref]] - end - - grouped_options_for_select(options, @ref || @project.default_branch) - end - # Define whenever show last push event # with suggestion to create MR def show_last_push_widget?(event) @@ -132,7 +116,7 @@ module ApplicationHelper return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? # Skip if user removed branch right after that - return false unless project.repository.branch_names.include?(event.branch_name) + return false unless project.repository.branch_exists?(event.branch_name) true end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 5b54b34070c..4b4bc3d4276 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -29,7 +29,7 @@ module BlobHelper if !on_top_of_branch?(project, ref) button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) - link_to "Edit", edit_path, class: 'btn btn-file-option' + link_to "Edit", edit_path, class: 'btn btn-sm' elsif can?(current_user, :fork_project, project) continue_params = { to: edit_path, @@ -186,12 +186,16 @@ module BlobHelper end def gitignore_names - return @gitignore_names if defined?(@gitignore_names) + @gitignore_names ||= + Gitlab::Template::Gitignore.categories.keys.map do |k| + [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }] + end.to_h + end - @gitignore_names = { - Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } }, - # Note that the key here doesn't cover it really - Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } } - } + def gitlab_ci_ymls + @gitlab_ci_ymls ||= + Gitlab::Template::GitlabCiYml.categories.keys.map do |k| + [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }] + end.to_h end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 3ee3fc74f0c..c533659b600 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -10,7 +10,7 @@ module BranchesHelper end def can_push_branch?(project, branch_name) - return false unless project.repository.branch_names.include?(branch_name) + return false unless project.repository.branch_exists?(branch_name) ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 067a00660aa..a0dafc52622 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -185,4 +185,17 @@ module GitlabMarkdownHelper '' end end + + def markdown_toolbar_button(options = {}) + data = options[:data].merge({ container: "body" }) + content_tag :button, + type: "button", + class: "toolbar-btn js-md has-tooltip hidden-xs", + tabindex: -1, + data: data, + title: options[:title], + aria: { label: options[:title] } do + icon(options[:icon]) + end + end end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 6dde2e9847d..45311690293 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -12,6 +12,11 @@ module Emails @member_id = member_id admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) + # A project in a group can have no explicit owners/masters, in that case + # we fallbacks to the group's owners/masters. + if admins.empty? && member_source.respond_to?(:group) && member_source.group + admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) + end mail(to: admins, subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}")) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d618c84e983..2b0bec33131 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -300,18 +300,12 @@ module Ci project.valid_runners_token? token end - def can_be_served?(runner) - return false unless has_tags? || runner.run_untagged? - - (tag_list - runner.tag_list).empty? - end - def has_tags? tag_list.any? end def any_runners_online? - project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } + project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) } end def stuck? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5b264ecffc5..ca5a685dd11 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -37,22 +37,22 @@ module Ci end def git_author_name - commit_data.author_name if commit_data + commit.try(:author_name) end def git_author_email - commit_data.author_email if commit_data + commit.try(:author_email) end def git_commit_message - commit_data.message if commit_data + commit.try(:message) end def short_sha Ci::Pipeline.truncate_sha(sha) end - def commit_data + def commit @commit ||= project.commit(sha) rescue nil diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index adb65292208..b64ec79ec2b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -4,7 +4,7 @@ module Ci LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = %w[specific shared active paused online] - FORM_EDITABLE = %i[description tag_list active run_untagged] + FORM_EDITABLE = %i[description tag_list active run_untagged locked] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' @@ -26,6 +26,13 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end + scope :assignable_for, ->(project) do + # FIXME: That `to_sql` is needed to workaround a weird Rails bug. + # Without that, placeholders would miss one and couldn't match. + where(locked: false). + where.not("id IN (#{project.runners.select(:id).to_sql})").specific + end + validate :tag_constraints acts_as_taggable @@ -56,7 +63,7 @@ module Ci def assign_to(project, current_user = nil) self.is_shared = false if shared? self.save - project.runner_projects.create!(runner_id: self.id) + project.runner_projects.create(runner_id: self.id) end def display_name @@ -91,6 +98,10 @@ module Ci !shared? end + def can_pick?(build) + assignable_for?(build.project) && accepting_tags?(build) + end + def only_for?(project) projects == [project] end @@ -111,5 +122,13 @@ module Ci 'can not be empty when runner is not allowed to pick untagged jobs') end end + + def assignable_for?(project) + !locked? || projects.exists?(id: project.id) + end + + def accepting_tags?(build) + (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty? + end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index d69d518fadd..174ccbaea6c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -271,6 +271,32 @@ class Commit merged_merge_request ? 'merge request' : 'commit' end + # Get the URI type of the given path + # + # Used to build URLs to files in the repository in GFM. + # + # path - String path to check + # + # Examples: + # + # uri_type('doc/README.md') # => :blob + # uri_type('doc/logo.png') # => :raw + # uri_type('doc/api') # => :tree + # uri_type('not/found') # => :nil + # + # Returns a symbol + def uri_type(path) + entry = @raw.tree.path(path) + if entry[:type] == :blob + blob = Gitlab::Git::Blob.new(name: entry[:name]) + blob.image? ? :raw : :blob + else + entry[:type] + end + rescue Rugged::TreeError + nil + end + private def repo_changes diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ab13db4b297..e437e3417a8 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -8,6 +8,8 @@ class CommitStatus < ActiveRecord::Base belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true belongs_to :user + delegate :commit, to: :pipeline + validates :pipeline, presence: true, unless: :importing? validates_presence_of :name diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 9056722f45e..9822844357d 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -53,6 +53,16 @@ module Participable # # Returns an Array of User instances. def participants(current_user = nil) + @participants ||= Hash.new do |hash, user| + hash[user] = raw_participants(user) + end + + @participants[current_user] + end + + private + + def raw_participants(current_user = nil) current_user ||= author ext = Gitlab::ReferenceExtractor.new(project, current_user) participants = Set.new diff --git a/app/models/repository.rb b/app/models/repository.rb index bbd7682d8e7..221c87164ca 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -191,8 +191,12 @@ class Repository end end + def ref_names + branch_names + tag_names + end + def branch_names - cache.fetch(:branch_names) { branches.map(&:name) } + @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) } end def branch_exists?(branch_name) @@ -267,6 +271,7 @@ class Repository def expire_branches_cache cache.expire(:branch_names) + @branch_names = nil @local_branches = nil end @@ -332,10 +337,6 @@ class Repository @lookup_cache ||= {} end - def expire_branch_names - cache.expire(:branch_names) - end - def expire_avatar_cache(branch_name = nil, revision = nil) # Avatars are pulled from the default branch, thus if somebody pushes to a # different branch there's no need to expire anything. diff --git a/app/models/user.rb b/app/models/user.rb index 2e458329cb9..876ccc69d8d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -487,9 +487,8 @@ class User < ActiveRecord::Base events.recent.find do |event| project = Project.find_by_id(event.project_id) next unless project - repo = project.repository - if repo.branch_names.include?(event.branch_name) + if project.repository.branch_exists?(event.branch_name) merge_requests = MergeRequest.where("created_at >= ?", event.created_at). where(source_project_id: project.id, source_branch: event.branch_name) diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index f0ed09a629a..9a187f5d694 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -21,7 +21,7 @@ module Ci end build = builds.find do |build| - build.can_be_served?(current_runner) + current_runner.can_pick?(build) end if build diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index e049b40bfab..61abfc6ecbe 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -28,7 +28,7 @@ .col-md-6 %h4 Restrict projects for this runner - if @runner.projects.any? - %table.table + %table.table.assigned-projects %thead %tr %th Assigned projects @@ -44,7 +44,7 @@ .pull-right = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' - %table.table + %table.table.unassigned-projects %thead %tr %th Project diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index d35f332e1e0..f7abad54286 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -13,7 +13,7 @@ Explore Projects .nav-controls - = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" = render 'shared/projects/dropdown' - if current_user.can_create_project? diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 62ebd69485c..aecefbc6e8f 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -33,7 +33,7 @@ = link_to "#shared", 'data-toggle' => 'tab' do Shared Projects .nav-controls - = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false = render 'shared/projects/dropdown' - if can? current_user, :create_projects, @group diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 01648047ce2..8cc0b59edeb 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -28,8 +28,12 @@ .key ⌘ shift p - else .key ctrl shift p - %td Toggle Markdown preview + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Edit last comment (when focused on an empty textarea) %tbody %tr %th diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 199ab3c38c3..2234bf79c87 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -13,7 +13,7 @@ = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username - = link_to '#', class: "nav-header-btn text-center pin-nav-btn #{'is-active' if pinned_nav?} js-nav-pin", title: 'Pin/Unpin navigation' do + = link_to '#', class: "nav-header-btn text-center pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do %span.sr-only Toggle navigation pinning = icon('thumb-tack') - if defined?(nav) && nav diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ffdc7b7f504..66e5ec1ad1a 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -3,6 +3,7 @@ %ul.nav-links.scrolling-tabs %li.fade-left + = icon('arrow-left') = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span @@ -37,3 +38,4 @@ %span Spam Logs %li.fade-right + = icon('arrow-right') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 3bd04c2c8b4..f7aa9fab7cf 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -3,6 +3,7 @@ %ul.nav-links.scrolling-tabs %li.fade-left + = icon('arrow-left') = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do %span @@ -32,3 +33,4 @@ %span Members %li.fade-right + = icon('arrow-right') diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 110a72d3a3c..44ea939b7e4 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,5 +1,6 @@ %ul.nav-links.scrolling-tabs %li.fade-left + = icon('arrow-left') = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do %span @@ -44,3 +45,4 @@ %span Audit Log %li.fade-right + = icon('arrow-right') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 823051d1b83..1ace5d23ba3 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -26,6 +26,7 @@ %div{ class: nav_control_class } %ul.nav-links.scrolling-tabs %li.fade-left + = icon('arrow-left') = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %span @@ -110,3 +111,4 @@ = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do Commits %li.fade-right + = icon('arrow-right') diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 5afd83a522e..f77738f97f5 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -28,7 +28,7 @@ = label_tag :global_notification_level, "Global notification level", class: "label-light" %br .clearfix - .form-group.pull-left + .form-group.pull-left.global-notification-setting = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true .clearfix diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 28a28282fd3..ca6714ef42b 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -14,8 +14,17 @@ %span This is a confidential issue. Your comment will not be visible to the public. %li.pull-right - %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } - Go full screen + .toolbar-group + = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) + = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) + = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) + = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`" }, title: "Insert code" }) + = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) + = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) + = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) + .toolbar-group + %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } + =icon("arrows-alt fw") .md-write-holder = yield @@ -24,7 +33,7 @@ - if defined?(referenced_users) && referenced_users %div.referenced-users.hide %span - = icon('exclamation-triangle') + = icon("exclamation-triangle") You are about to add %strong %span.js-referenced-users-count 0 diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml index ee63bc55a30..ac80951dd4f 100644 --- a/app/views/projects/badges/index.html.haml +++ b/app/views/projects/badges/index.html.haml @@ -7,7 +7,7 @@ %b Builds badge · = @build_badge.to_html .pull-right - = render 'shared/ref_switcher', destination: 'badges' + = render 'shared/ref_switcher', destination: 'badges', align_right: true .panel-body .row .col-md-2.text-center diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index ae89637df60..29c7d45074a 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -17,6 +17,8 @@ = dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) .gitignore-selector.js-gitignore-selector-wrap.hidden = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden + = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index b8d8758fd2b..e38d1ff5ff0 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -24,8 +24,8 @@ %span.label.label-warning stuck %p.commit-title - - if commit_data = pipeline.commit_data - = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" + - if commit = pipeline.commit + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index a959b34a539..929496f81d8 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -10,29 +10,30 @@ = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } = commit_author_avatar(commit, size: 36) - .commit-row-title - %span.item-title - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - %span.commit-row-message.visible-xs-inline - · - = commit.short_id - - if commit.status - = render_commit_status(commit, cssclass: 'visible-xs-inline') - - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + .commit-info-block + .commit-row-title + %span.item-title + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status + = render_commit_status(commit, cssclass: 'visible-xs-inline') + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - .commit-actions.hidden-xs - - if commit.status - = render_commit_status(commit, cssclass: 'btn btn-transparent') - = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent') - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" - = link_to_browse_code(project, commit) + .commit-actions.hidden-xs + - if commit.status + = render_commit_status(commit, cssclass: 'btn btn-transparent') + = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent') + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) - - if commit.description? - %pre.commit-row-description.js-toggle-content - = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) + - if commit.description? + %pre.commit-row-description.js-toggle-content + = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) - .commit-row-info - = commit_author_link(commit, avatar: false, size: 24) - authored - #{time_ago_with_tooltip(commit.committed_date)} + .commit-row-info + = commit_author_link(commit, avatar: false, size: 24) + authored + #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 888c6b6701b..54dab4bff07 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -2,6 +2,7 @@ .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } %li.fade-left + = icon('arrow-left') = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project) do Files @@ -26,3 +27,4 @@ = link_to namespace_project_tags_path(@project.namespace, @project) do Tags %li.fade-right + = icon('arrow-right') diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index f35faa6afb5..10822b6184c 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -3,9 +3,9 @@ = escape_once(tag.name) = clipboard_button(clipboard_text: "docker pull #{tag.path}") %td - - if layer = tag.layers.first - %span.has-tooltip{ title: "#{layer.revision}" } - = layer.short_revision + - if tag.revision + %span.has-tooltip{ title: "#{tag.revision}" } + = tag.short_revision - else \- %td diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index ae9e77e7d89..a03f117291f 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,16 +3,24 @@ = render "projects/pipelines/head" %div{ class: (container_class) } - - if can?(current_user, :create_environment, @project) + - if can?(current_user, :create_environment, @project) && !@environments.blank? .top-area .nav-controls = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - if @environments.blank? - %ul.content-list.environments - %li.nothing-here-block - No environments to show + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any environments right now. + %p.blank-state-text + Environments are places where code gets deployed, such as staging or production. + %br + = succeed "." do + = link_to "Read more about environments", help_page_path("ci", "environments") + - if can?(current_user, :create_environment, @project) + = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do + New environment - else .table-holder %table.table.environments diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 54465828ba9..da325efecd2 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -4,6 +4,9 @@ .col-lg-3 %h4.prepend-top-0 New Environment - %p Environments allow you to track deployments of your application + %p + Environments allow you to track deployments of your application + = succeed "." do + = link_to "Read more about environments", help_page_path("ci", "environments") = render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 069b77b5adf..4c15e2759d6 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -13,10 +13,14 @@ = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - if @deployments.blank? - %ul.content-list.environments - %li.nothing-here-block - No deployments for - %strong= @environment.name + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any deployments right now. + %p.blank-state-text + Define environments in the deploy stage(s) in + %code .gitlab-ci.yml + to track deployments here. + = link_to "Read more", help_page_path("ci", "environments"), class: "btn btn-success" - else .table-holder %table.table.environments diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 393998f15b9..53dd300c35c 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,8 +1,8 @@ - content_for :note_actions do - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"} + = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"} - if @merge_request.closed? - = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} + = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 0b002043408..7d1cbc62e86 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -5,4 +5,4 @@ is supported %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } = icon('file-image-o', class: 'toolbar-button-icon') - Attach a file + Attach a file
\ No newline at end of file diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index bcdbff08011..c04d291412c 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -18,9 +18,9 @@ = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') .note-actions - access = note.project.team.human_max_access(note.author.id) - - if access + - if access and not note.system %span.note-role.hidden-xs= access - - if current_user + - if current_user and not note.system = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') = icon('smile-o') diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index d62f5c8f131..c45a9d4f81f 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -13,6 +13,12 @@ = f.check_box :run_untagged %span.light Indicates whether this runner can pick jobs without tags .form-group + = label :locked, 'Lock to current projects', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :locked + %span.light When a runner is locked, it cannot be assigned to other projects + .form-group = label_tag :token, class: 'control-label' do Token .col-sm-10 diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 96e2aac451f..85225857758 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -2,8 +2,10 @@ %h4 = runner_status_icon(runner) %span.monospace - - if @runners.include?(runner) + - if @project_runners.include?(runner) = link_to runner.short_sha, runner_path(runner) + - if runner.locked? + = icon('lock', class: 'has-tooltip', title: 'Locked to current projects') %small = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do %i.fa.fa-edit.btn @@ -11,7 +13,7 @@ = runner.short_sha .pull-right - - if @runners.include?(runner) + - if @project_runners.include?(runner) - if runner.belongs_to_one_project? = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 8ae9f0d95f7..d469dda5b81 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -17,13 +17,13 @@ Start runner! -- if @runners.any? +- if @project_runners.any? %h4.underlined-title Runners activated for this project %ul.bordered-list.activated-specific-runners - = render partial: 'runner', collection: @runners, as: :runner + = render partial: 'runner', collection: @project_runners, as: :runner -- if @specific_runners.any? +- if @assignable_runners.any? %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners - = render partial: 'runner', collection: @specific_runners, as: :runner - = paginate @specific_runners + = render partial: 'runner', collection: @assignable_runners, as: :runner + = paginate @assignable_runners diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index f24e1b9144e..61b99f35d74 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -23,6 +23,9 @@ %td Can run untagged jobs %td= @runner.run_untagged? ? 'Yes' : 'No' %tr + %td Locked to this project + %td= @runner.locked? ? 'Yes' : 'No' + %tr %td Tags %td - @runner.tag_list.each do |tag| diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index e9ca46a74bf..5f041aedfc0 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -57,6 +57,10 @@ %li.missing = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set up CI - if @repository.commit .content-block.second-block.white diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 6f9809a9ed8..aa18e6f236f 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,7 +1,9 @@ %ul.nav-links.event-filter.scrolling-tabs %li.fade-left + = icon('arrow-left') = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' %li.fade-right + = icon('arrow-right') diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index eb2e1919e19..ea7162d4d63 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,7 +1,14 @@ +- dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do - = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm" = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil + .dropdown + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" } + .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } + = dropdown_title "Switch branch/tag" + = dropdown_filter "Search branches and tags" + = dropdown_content + = dropdown_loading diff --git a/config/routes.rb b/config/routes.rb index de6094fa0ed..e45293cdf7f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -295,7 +295,7 @@ Rails.application.routes.draw do post :repository_check end - resources :runner_projects + resources :runner_projects, only: [:create, :destroy] end end @@ -479,6 +479,7 @@ Rails.application.routes.draw do get :download_export get :autocomplete_sources get :activity + get :refs end scope module: :projects do diff --git a/db/migrate/20160509091049_add_locked_to_ci_runner.rb b/db/migrate/20160509091049_add_locked_to_ci_runner.rb new file mode 100644 index 00000000000..3fbaef3b7f0 --- /dev/null +++ b/db/migrate/20160509091049_add_locked_to_ci_runner.rb @@ -0,0 +1,13 @@ +class AddLockedToCiRunner < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:ci_runners, :locked, :boolean, + default: false, allow_null: false) + end + + def down + remove_column(:ci_runners, :locked) + end +end diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb new file mode 100644 index 00000000000..bd0463886bc --- /dev/null +++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb @@ -0,0 +1,9 @@ +class SetMissingStageOnCiBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def up + update_column_in_batches(:ci_builds, :stage, :test) do |table, query| + query.where(table[:stage].eq(nil)) + end + end +end diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb new file mode 100644 index 00000000000..dfa5110dea4 --- /dev/null +++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexOnRunnersLocked < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :ci_runners, :locked + end +end diff --git a/db/schema.rb b/db/schema.rb index 4c1eb8cc1b3..7a8377f687c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160616103948) do +ActiveRecord::Schema.define(version: 20160620115026) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -287,9 +287,11 @@ ActiveRecord::Schema.define(version: 20160616103948) do t.string "platform" t.string "architecture" t.boolean "run_untagged", default: true, null: false + t.boolean "locked", default: false, null: false end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"} @@ -708,6 +710,7 @@ ActiveRecord::Schema.define(version: 20160616103948) do t.integer "level", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "events" end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree diff --git a/doc/ci/README.md b/doc/ci/README.md index ef72df97ce6..5a1cb5319c6 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -5,6 +5,7 @@ - [Get started with GitLab CI](quick_start/README.md) - [CI examples for various languages](examples/README.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) +- [Environments and deployments](environments.md) - [Learn how `.gitlab-ci.yml` works](yaml/README.md) - [Configure a Runner, the application that runs your builds](runners/README.md) - [Use Docker images with GitLab Runner](docker/using_docker_images.md) diff --git a/doc/ci/environments.md b/doc/ci/environments.md new file mode 100644 index 00000000000..040379bb381 --- /dev/null +++ b/doc/ci/environments.md @@ -0,0 +1,58 @@ +# Introduction to environments and deployments + +>**Note:** +Introduced in GitLab 8.9. + +## Environments + +Environments are places where code gets deployed, such as staging or production. +CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment. +Defining environments in a project's `.gitlab-ci.yml` lets developers track +[deployments] to these environments. + +## Deployments + +Deployments are created when [jobs] deploy versions of code to [environments]. + +## Defining environments + +You can create and delete environments manually in the web interface, but we +recommend that you define your environments in `.gitlab-ci.yml` first, which +will automatically create environments for you after the first deploy. + +The `environment` is just a hint for GitLab that this job actually deploys to +this environment. Each time the job succeeds, a deployment is recorded, +remembering the git SHA and environment. + +Add something like this to your `.gitlab-ci.yml`: +``` +production: + stage: deploy + script: dpl... + environment: production +``` + +See full [documentation](yaml/README.md#environment). + +## Seeing environment status + +You can find the environment list under **Pipelines > Environments** for your +project. You'll see the git SHA and date of the last deployment to each +environment defined. + +>**Note:** +Only deploys that happen after your `.gitlab-ci.yml` is properly configured will +show up in the environments and deployments lists. + +## Seeing deployment history + +Clicking on an environment will show the history of deployments. + +>**Note:** +Only deploys that happen after your `.gitlab-ci.yml` is properly configured will +show up in the environments and deployments lists. + +[Pipelines]: quick_start/README.md +[jobs]: yaml/README.md#jobs +[environments]: #environments +[deployments]: #deployments diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 400784da617..ddebd987650 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -96,6 +96,12 @@ To register the runner, run the command below and follow instructions: sudo gitlab-ci-multi-runner register ``` +### Lock a specific runner from being enabled for other projects + +You can configure a runner to assign it exclusively to a project. When a +runner is locked this way, it can no longer be enabled for other projects. +This setting is available on each runner in *Project Settings* > *Runners*. + ### Making an existing Shared Runner Specific If you are an admin on your GitLab instance, @@ -128,7 +134,7 @@ the appropriate dependencies to run Rails test suites. ### Prevent runner with tags from picking jobs without tags You can configure a runner to prevent it from picking jobs with tags when -the runnner does not have tags assigned. This setting is available on each +the runner does not have tags assigned. This setting is available on each runner in *Project Settings* > *Runners*. ### Be careful with sensitive information diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 6053bf14536..d0fbcbe9988 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -530,14 +530,18 @@ The above script will: ### environment >**Note:** -Introduced in GitLab v8.9.0. +Introduced in GitLab 8.9. -`environment` is used to define that job does deployment to specific environment. -This allows to easily track all deployments to your environments straight from GitLab. +`environment` is used to define that a job deploys to a specific environment. +This allows easy tracking of all deployments to your environments straight from +GitLab. -If `environment` is specified and no environment under that name does exist a new one will be created automatically. +If `environment` is specified and no environment under that name exists, a new +one will be created automatically. -The `environment` name must contain only letters, digits, '-' and '_'. +The `environment` name must contain only letters, digits, '-' and '_'. Common +names are `qa`, `staging`, and `production`, but you can use whatever name works +with your workflow. --- @@ -550,7 +554,8 @@ deploy to production: environment: production ``` -The `deploy to production` job will be marked as doing deployment to `production` environment. +The `deploy to production` job will be marked as doing deployment to +`production` environment. ### artifacts diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md new file mode 100644 index 00000000000..2f9f36f962e --- /dev/null +++ b/doc/project_services/emails_on_push.md @@ -0,0 +1,17 @@ +## Enabling emails on push + +To receive email notifications for every change that is pushed to the project, visit +your project's **Settings > Services > Emails on push** and activate the service. + +In the _Recipients_ area, provide a list of emails separated by commas. + +You can configure any of the following settings depending on your preference. + ++ **Push events** - Email will be triggered when a push event is recieved ++ **Tag push events** - Email will be triggered when a tag is created and pushed ++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). ++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. + +--- + +![Email on push service settings](img/emails_on_push_service.png) diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png Binary files differnew file mode 100644 index 00000000000..cd6f79ad1eb --- /dev/null +++ b/doc/project_services/img/emails_on_push_service.png diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index a5af620d9be..f81a035f70b 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -33,7 +33,7 @@ further configuration instructions and details. Contributions are welcome. | Campfire | Simple web-based real-time group chat | | Custom Issue Tracker | Custom issue tracker | | Drone CI | Continuous Integration platform built on Docker, written in Go | -| Emails on push | Email the commits and diff of each push to a list of recipients | +| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md index bb463d43a7c..cb66ef920bb 100644 --- a/doc/update/8.6-to-8.7.md +++ b/doc/update/8.6-to-8.7.md @@ -45,7 +45,7 @@ sudo -u git -H git checkout 8-7-stable-ee ```bash cd /home/git/gitlab-shell -sudo -u git -H git fetch --all --tags +sudo -u git -H git fetch --tags sudo -u git -H git checkout v2.7.2 ``` diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md index fffa0aba57f..4b551130255 100644 --- a/doc/workflow/add-user/add-user.md +++ b/doc/workflow/add-user/add-user.md @@ -8,7 +8,7 @@ You should have `master` or `owner` permissions to add or import a new user to your project. The first step to add or import a user, go to your project and click on -**Members** on the left side of your screen. +**Members** in the drop-down menu on the right side of your screen. ![Members](img/add_user_members_menu.png) @@ -87,3 +87,25 @@ invitation, change their access level or even delete them. Once the user accepts the invitation, they will be prompted to create a new GitLab account using the same e-mail address the invitation was sent to. + +## Request access to a project + +As a user, you can request to be a member of a project. Go to the project you'd +like to be a member of, and click the **Request Access** button on the right +side of your screen. + +![Request access button](img/request_access_button.png) + +--- + +Project owners & masters will be notified of your request and will be able to approve or +decline it on the members page. + +![Manage access requests](img/access_requests_management.png) + +--- + +If you change your mind before your request is approved, just click the +**Withdraw Access Request** button. + +![Withdraw access request button](img/withdraw_access_request_button.png) diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png Binary files differnew file mode 100644 index 00000000000..e9641cb4f85 --- /dev/null +++ b/doc/workflow/add-user/img/access_requests_management.png diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png Binary files differindex 910affc9659..18aabf93d50 100644 --- a/doc/workflow/add-user/img/add_user_email_accept.png +++ b/doc/workflow/add-user/img/add_user_email_accept.png diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png Binary files differindex 5f02ce89b3e..385d64330c0 100644 --- a/doc/workflow/add-user/img/add_user_email_ready.png +++ b/doc/workflow/add-user/img/add_user_email_ready.png diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png Binary files differindex 140979fbe13..84741edbca4 100644 --- a/doc/workflow/add-user/img/add_user_email_search.png +++ b/doc/workflow/add-user/img/add_user_email_search.png diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png Binary files differindex 8ef9156c8d5..7e580384e54 100644 --- a/doc/workflow/add-user/img/add_user_give_permissions.png +++ b/doc/workflow/add-user/img/add_user_give_permissions.png diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png Binary files differindex 5770d5cf0c4..8dbd73a5bc8 100644 --- a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png +++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png Binary files differindex dea4b3f40ad..abac1f59c02 100644 --- a/doc/workflow/add-user/img/add_user_imported_members.png +++ b/doc/workflow/add-user/img/add_user_imported_members.png diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png Binary files differindex 7daa6ca7d9e..e17d88c6f5f 100644 --- a/doc/workflow/add-user/img/add_user_list_members.png +++ b/doc/workflow/add-user/img/add_user_list_members.png diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png Binary files differindex f1797b95f67..ec5d39f402d 100644 --- a/doc/workflow/add-user/img/add_user_members_menu.png +++ b/doc/workflow/add-user/img/add_user_members_menu.png diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png Binary files differindex 5ac10ce80d4..eaa062376f4 100644 --- a/doc/workflow/add-user/img/add_user_search_people.png +++ b/doc/workflow/add-user/img/add_user_search_people.png diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png Binary files differnew file mode 100644 index 00000000000..984d640b0f0 --- /dev/null +++ b/doc/workflow/add-user/img/request_access_button.png diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png Binary files differnew file mode 100644 index 00000000000..ff54a0e4384 --- /dev/null +++ b/doc/workflow/add-user/img/withdraw_access_request_button.png diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 34ada1774d8..1a316e80976 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -51,6 +51,28 @@ If necessary, you can increase the access level of an individual user for a spec ![Barry effectively has 'Master' access to GitLab CI now](groups/override_access_level.png) +## Request access to a group + +As a user, you can request to be a member of a group. Go to the group you'd +like to be a member of, and click the **Request Access** button on the right +side of your screen. + +![Request access button](groups/request_access_button.png) + +--- + +Group owners & masters will be notified of your request and will be able to approve or +decline it on the members page. + +![Manage access requests](groups/access_requests_management.png) + +--- + +If you change your mind before your request is approved, just click the +**Withdraw Access Request** button. + +![Withdraw access request button](groups/withdraw_access_request_button.png) + ## Managing group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png Binary files differnew file mode 100644 index 00000000000..ffede8e9bd6 --- /dev/null +++ b/doc/workflow/groups/access_requests_management.png diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png Binary files differnew file mode 100644 index 00000000000..ff0ac8747a7 --- /dev/null +++ b/doc/workflow/groups/request_access_button.png diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png Binary files differnew file mode 100644 index 00000000000..99d7a326ed8 --- /dev/null +++ b/doc/workflow/groups/withdraw_access_request_button.png diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png Binary files differindex beb6c53ec77..16be0413b64 100644 --- a/doc/workflow/shortcuts.png +++ b/doc/workflow/shortcuts.png diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index 979f4692d5a..7e339443b75 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -15,8 +15,6 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I should see Notification saved message' do - page.within '.flash-container' do - expect(page).to have_content 'Notification settings saved' - end + expect(page).to have_content 'On mention' end end diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 9b59b682676..019b3124a86 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -20,11 +20,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should select "master" in select box' do - expect(page).to have_selector '.select2-chosen', text: "master" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" end step 'page should select "v1.0.0" in select box' do - expect(page).to have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0" end step 'page should have "master" on graph' do @@ -40,11 +40,19 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end When 'I switch ref to "feature"' do - select 'feature', from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'feature' + end end When 'I switch ref to "v1.0.0"' do - select 'v1.0.0', from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'v1.0.0' + end end When 'click "Show only selected branch" checkbox' do @@ -68,11 +76,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should select "feature" in select box' do - expect(page).to have_selector '.select2-chosen', text: "feature" + expect(page).to have_selector '.dropdown-menu-toggle', text: "feature" end step 'page should select "v1.0.0" in select box' do - expect(page).to have_selector '.select2-chosen', text: "v1.0.0" + expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0" end step 'page should have "feature" on graph' do diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 98b57e5cbfb..76fefee9254 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -134,8 +134,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see Notification saved message' do - page.within '.flash-container' do - expect(page).to have_content 'Notification settings saved' + page.within '#notifications-button' do + expect(page).to have_content 'On mention' end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 79a3ed8197e..0fe046dcbf6 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -290,15 +290,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step "I switch ref to 'test'" do - select "'test'", from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'test' + end end step "I switch ref to fix" do - select "fix", from: 'ref' + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'fix' + end end step "I see the ref 'test' has been selected" do - expect(page).to have_selector '.select2-chosen', text: "'test'" + expect(page).to have_selector '.dropdown-toggle-text', text: "'test'" end step "I visit the 'test' tree" do diff --git a/lib/api/api.rb b/lib/api/api.rb index 0e7a1cc2623..f8f680a6311 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -33,7 +33,6 @@ module API mount ::API::Commits mount ::API::DeployKeys mount ::API::Files - mount ::API::Gitignores mount ::API::GroupMembers mount ::API::Groups mount ::API::Internal @@ -58,6 +57,7 @@ module API mount ::API::Subscriptions mount ::API::SystemHooks mount ::API::Tags + mount ::API::Templates mount ::API::Triggers mount ::API::Users mount ::API::Variables diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2e397643ed1..5a23a18fe9c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -423,6 +423,7 @@ module API class RunnerDetails < Runner expose :tag_list expose :run_untagged + expose :locked expose :version, :revision, :platform, :architecture expose :contacted_at expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } @@ -444,11 +445,7 @@ module API expose :created_at, :started_at, :finished_at expose :user, with: User expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } - expose :commit, with: RepoCommit do |repo_obj, _options| - if repo_obj.respond_to?(:commit) - repo_obj.commit.commit_data - end - end + expose :commit, with: RepoCommit expose :runner, with: Runner end @@ -472,11 +469,11 @@ module API expose :content end - class GitignoresList < Grape::Entity + class TemplatesList < Grape::Entity expose :name end - class Gitignore < Grape::Entity + class Template < Grape::Entity expose :name, :content end end diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb deleted file mode 100644 index 270c9501dd2..00000000000 --- a/lib/api/gitignores.rb +++ /dev/null @@ -1,29 +0,0 @@ -module API - class Gitignores < Grape::API - - # Get the list of the available gitignore templates - # - # Example Request: - # GET /gitignores - get 'gitignores' do - present Gitlab::Gitignore.all, with: Entities::GitignoresList - end - - # Get the text for a specific gitignore - # - # Parameters: - # name (required) - The name of a license - # - # Example Request: - # GET /gitignores/Elixir - # - get 'gitignores/:name' do - required_attributes! [:name] - - gitignore = Gitlab::Gitignore.find(params[:name]) - not_found!('.gitignore') unless gitignore - - present gitignore, with: Entities::Gitignore - end - end -end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 3ac7b50c4ce..1d361569d59 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -23,8 +23,6 @@ module API end post "/allowed" do - Gitlab::Metrics.action = 'Grape#/internal/allowed' - status 200 actor = diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 4faba9dc87b..ecc8f2fc5a2 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -49,7 +49,7 @@ module API runner = get_runner(params[:id]) authenticate_update_runner!(runner) - attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged] + attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked] if runner.update(attrs) present runner, with: Entities::RunnerDetails, current_user: current_user else @@ -96,9 +96,14 @@ module API runner = get_runner(params[:runner_id]) authenticate_enable_runner!(runner) - Ci::RunnerProject.create(runner: runner, project: user_project) - present runner, with: Entities::Runner + runner_project = runner.assign_to(user_project) + + if runner_project.persisted? + present runner, with: Entities::Runner + else + conflict!("Runner was already enabled for this project") + end end # Disable project's runner @@ -163,6 +168,7 @@ module API def authenticate_enable_runner!(runner) forbidden!("Runner is shared") if runner.is_shared? + forbidden!("Runner is locked") if runner.locked? return if current_user.is_admin? forbidden!("No access granted") unless user_can_access_runner?(runner) end diff --git a/lib/api/templates.rb b/lib/api/templates.rb new file mode 100644 index 00000000000..18408797756 --- /dev/null +++ b/lib/api/templates.rb @@ -0,0 +1,36 @@ +module API + class Templates < Grape::API + TEMPLATE_TYPES = { + gitignores: Gitlab::Template::Gitignore, + gitlab_ci_ymls: Gitlab::Template::GitlabCiYml + }.freeze + + TEMPLATE_TYPES.each do |template, klass| + # Get the list of the available template + # + # Example Request: + # GET /gitignores + # GET /gitlab_ci_ymls + get template.to_s do + present klass.all, with: Entities::TemplatesList + end + + # Get the text for a specific template + # + # Parameters: + # name (required) - The name of a template + # + # Example Request: + # GET /gitignores/Elixir + # GET /gitlab_ci_ymls/Ruby + get "#{template}/:name" do + required_attributes! [:name] + + new_template = klass.find(params[:name]) + not_found!(template.to_s.singularize) unless new_template + + present new_template, with: Entities::Template + end + end + end +end diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index ea21c7b041c..c78da404607 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -14,6 +14,8 @@ module Banzai def call return doc unless linkable_files? + @uri_types = {} + doc.search('a:not(.gfm)').each do |el| process_link_attr el.attribute('href') end @@ -48,7 +50,7 @@ module Banzai uri.path = [ relative_url_root, context[:project].path_with_namespace, - path_type(file_path), + uri_type(file_path), ref || context[:project].default_branch, # if no ref exists, point to the default branch file_path ].compact.join('/').squeeze('/').chomp('/') @@ -87,7 +89,7 @@ module Banzai return path unless request_path parts = request_path.split('/') - parts.pop if path_type(request_path) != 'tree' + parts.pop if uri_type(request_path) != :tree while path.start_with?('../') parts.pop @@ -98,45 +100,20 @@ module Banzai end def file_exists?(path) - return false if path.nil? - repository.blob_at(current_sha, path).present? || - repository.tree(current_sha, path).entries.any? - end - - # Get the type of the given path - # - # path - String path to check - # - # Examples: - # - # path_type('doc/README.md') # => 'blob' - # path_type('doc/logo.png') # => 'raw' - # path_type('doc/api') # => 'tree' - # - # Returns a String - def path_type(path) - unescaped_path = Addressable::URI.unescape(path) - - if tree?(unescaped_path) - 'tree' - elsif image?(unescaped_path) - 'raw' - else - 'blob' - end + path.present? && !!uri_type(path) end - def tree?(path) - repository.tree(current_sha, path).entries.any? - end + def uri_type(path) + @uri_types[path] ||= begin + unescaped_path = Addressable::URI.unescape(path) - def image?(path) - repository.blob_at(current_sha, path).try(:image?) + current_commit.uri_type(unescaped_path) + end end - def current_sha - context[:commit].try(:id) || - ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha + def current_commit + @current_commit ||= context[:commit] || + ref ? repository.commit(ref) : repository.head_commit end def relative_url_root @@ -148,7 +125,7 @@ module Banzai end def repository - context[:project].try(:repository) + @repository ||= context[:project].try(:repository) end end end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 0c41f22c7c5..bcc82969eb3 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -28,12 +28,9 @@ module Ci post "register" do required_attributes! [:token] - attributes = { description: params[:description], - tag_list: params[:tag_list] } - - unless params[:run_untagged].nil? - attributes[:run_untagged] = params[:run_untagged] - end + attributes = attributes_for_keys( + [:description, :tag_list, :run_untagged, :locked] + ) runner = if runner_registration_token_valid? diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 7a0929d774e..708d01b95a1 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -3,6 +3,7 @@ module ContainerRegistry attr_reader :repository, :name delegate :registry, :client, to: :repository + delegate :revision, :short_revision, to: :config_blob, allow_nil: true def initialize(repository, name) @repository, @name = repository, name diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 7e3f5abba62..ab7b811c5d8 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -31,7 +31,7 @@ module Grack auth! - lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call + lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call return lfs_response unless lfs_response.nil? if @user.nil? && !@ci diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb deleted file mode 100644 index f46b43b61a4..00000000000 --- a/lib/gitlab/gitignore.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Gitlab - class Gitignore - FILTER_REGEX = /\.gitignore\z/.freeze - - def initialize(path) - @path = path - end - - def name - File.basename(@path, '.gitignore') - end - - def content - File.read(@path) - end - - class << self - def all - languages_frameworks + global - end - - def find(key) - file_name = "#{key}.gitignore" - - directory = select_directory(file_name) - directory ? new(File.join(directory, file_name)) : nil - end - - def global - files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) } - end - - def languages_frameworks - files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) } - end - - private - - def select_directory(file_name) - [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) } - end - - def global_dir - File.join(gitignore_dir, 'Global') - end - - def gitignore_dir - Rails.root.join('vendor/gitignore') - end - - def files_for_folder(dir) - Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') } - end - end - end -end diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb index 9d9617761b3..e3ed2f6791d 100644 --- a/lib/gitlab/lfs/response.rb +++ b/lib/gitlab/lfs/response.rb @@ -2,10 +2,11 @@ module Gitlab module Lfs class Response - def initialize(project, user, request) + def initialize(project, user, ci, request) @origin_project = project @project = storage_project(project) @user = user + @ci = ci @env = request.env @request = request end @@ -189,7 +190,7 @@ module Gitlab return render_not_enabled unless Gitlab.config.lfs.enabled unless @project.public? - return render_unauthorized unless @user + return render_unauthorized unless @user || @ci return render_forbidden unless user_can_fetch? end @@ -210,7 +211,7 @@ module Gitlab def user_can_fetch? # Check user access against the project they used to initiate the pull - @user.can?(:download_code, @origin_project) + @ci || @user.can?(:download_code, @origin_project) end def user_can_push? diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb index 78d02891102..69bd5e62305 100644 --- a/lib/gitlab/lfs/router.rb +++ b/lib/gitlab/lfs/router.rb @@ -1,9 +1,12 @@ module Gitlab module Lfs class Router - def initialize(project, user, request) + attr_reader :project, :user, :ci, :request + + def initialize(project, user, ci, request) @project = project @user = user + @ci = ci @env = request.env @request = request end @@ -80,7 +83,7 @@ module Gitlab def lfs return unless @project - Gitlab::Lfs::Response.new(@project, @user, @request) + Gitlab::Lfs::Response.new(@project, @user, @ci, @request) end def sanitize_tmp_filename(name) diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb new file mode 100644 index 00000000000..760ff3e614a --- /dev/null +++ b/lib/gitlab/template/base_template.rb @@ -0,0 +1,67 @@ +module Gitlab + module Template + class BaseTemplate + def initialize(path) + @path = path + end + + def name + File.basename(@path, self.class.extension) + end + + def content + File.read(@path) + end + + class << self + def all + self.categories.keys.flat_map { |cat| by_category(cat) } + end + + def find(key) + file_name = "#{key}#{self.extension}" + + directory = select_directory(file_name) + directory ? new(File.join(category_directory(directory), file_name)) : nil + end + + def categories + raise NotImplementedError + end + + def extension + raise NotImplementedError + end + + def base_dir + raise NotImplementedError + end + + def by_category(category) + templates_for_directory(category_directory(category)) + end + + def category_directory(category) + File.join(base_dir, categories[category]) + end + + private + + def select_directory(file_name) + categories.keys.find do |category| + File.exist?(File.join(category_directory(category), file_name)) + end + end + + def templates_for_directory(dir) + dir << '/' unless dir.end_with?('/') + Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) } + end + + def filter_regex + @filter_reges ||= /#{Regexp.escape(extension)}\z/ + end + end + end + end +end diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore.rb new file mode 100644 index 00000000000..964fbfd4de3 --- /dev/null +++ b/lib/gitlab/template/gitignore.rb @@ -0,0 +1,22 @@ +module Gitlab + module Template + class Gitignore < BaseTemplate + class << self + def extension + '.gitignore' + end + + def categories + { + "Languages" => '', + "Global" => 'Global' + } + end + + def base_dir + Rails.root.join('vendor/gitignore') + end + end + end + end +end diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml.rb new file mode 100644 index 00000000000..7f480fe33c0 --- /dev/null +++ b/lib/gitlab/template/gitlab_ci_yml.rb @@ -0,0 +1,27 @@ +module Gitlab + module Template + class GitlabCiYml < BaseTemplate + def content + explanation = "# This file is a template, and might need editing before it works on your project." + [explanation, super].join("\n") + end + + class << self + def extension + '.gitlab-ci.yml' + end + + def categories + { + "General" => '', + "Pages" => 'Pages' + } + end + + def base_dir + Rails.root.join('vendor/gitlab-ci-yml') + end + end + end + end +end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake deleted file mode 100644 index 4fd48cccb1d..00000000000 --- a/lib/tasks/gitlab/update_gitignore.rake +++ /dev/null @@ -1,46 +0,0 @@ -namespace :gitlab do - desc "GitLab | Update gitignore" - task :update_gitignore do - unless clone_gitignores - puts "Cloning the gitignores failed".color(:red) - return - end - - remove_unneeded_files(gitignore_directory) - remove_unneeded_files(global_directory) - - puts "Done".color(:green) - end - - def clone_gitignores - FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory) - FileUtils.cd vendor_directory - - system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git') - end - - # Retain only certain files: - # - The LICENSE, because we have to - # - The sub dir global - # - The gitignores themself - # - Dir.entires returns also the entries '.' and '..' - def remove_unneeded_files(path) - Dir.foreach(path) do |file| - FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ - end - end - - private - - def vendor_directory - Rails.root.join('vendor') - end - - def gitignore_directory - File.join(vendor_directory, 'gitignore') - end - - def global_directory - File.join(gitignore_directory, 'Global') - end -end diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake new file mode 100644 index 00000000000..4f76dad7286 --- /dev/null +++ b/lib/tasks/gitlab/update_templates.rake @@ -0,0 +1,54 @@ +namespace :gitlab do + desc "GitLab | Update templates" + task :update_templates do + TEMPLATE_DATA.each { |template| update(template) } + end + + def update(template) + sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1] + dir = File.join(vendor_directory, sub_dir) + + unless clone_repository(template.repo_url, dir) + puts "Cloning the #{sub_dir} templates failed".red + return + end + + remove_unneeded_files(dir, template.cleanup_regex) + puts "Done".green + end + + def clone_repository(url, directory) + FileUtils.rm_rf(directory) if Dir.exist?(directory) + + system("git clone #{url} --depth=1 --branch=master #{directory}") + end + + # Retain only certain files: + # - The LICENSE, because we have to + # - The sub dirs so we can organise the file by category + # - The templates themself + # - Dir.entries returns also the entries '.' and '..' + def remove_unneeded_files(directory, regex) + Dir.foreach(directory) do |file| + FileUtils.rm_rf(File.join(directory, file)) unless file =~ regex + end + end + + private + + Template = Struct.new(:repo_url, :cleanup_regex) + TEMPLATE_DATA = [ + Template.new( + "https://github.com/github/gitignore.git", + /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ + ), + Template.new( + "https://gitlab.com/gitlab-org/gitlab-ci-yml.git", + /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/ + ) + ] + + def vendor_directory + Rails.root.join('vendor') + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index fba545560c7..146b2c2e131 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -237,4 +237,24 @@ describe ProjectsController do expect(response.status).to eq(401) end end + + describe "GET refs" do + it "should get a list of branches and tags" do + get :refs, namespace_id: public_project.namespace.path, id: public_project.path + + parsed_body = JSON.parse(response.body) + expect(parsed_body["Branches"]).to include("master") + expect(parsed_body["Tags"]).to include("v1.0.0") + expect(parsed_body["Commits"]).to be_nil + end + + it "should get a list of branches, tags and commits" do + get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456" + + parsed_body = JSON.parse(response.body) + expect(parsed_body["Branches"]).to include("master") + expect(parsed_body["Tags"]).to include("v1.0.0") + expect(parsed_body["Commits"]).to include("123456") + end + end end diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/pipelines.rb index a039bef6f3c..a039bef6f3c 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/pipelines.rb diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 9499cd4e025..2d297776cb0 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -60,6 +60,40 @@ describe "Admin Runners" do it { expect(page).to have_content(@project1.name_with_namespace) } it { expect(page).not_to have_content(@project2.name_with_namespace) } end + + describe 'enable/create' do + before do + @project1.runners << runner + visit admin_runner_path(runner) + end + + it 'enables specific runner for project' do + within '.unassigned-projects' do + click_on 'Enable' + end + + assigned_project = page.find('.assigned-projects') + + expect(assigned_project).to have_content(@project2.path) + end + end + + describe 'disable/destroy' do + before do + @project1.runners << runner + visit admin_runner_path(runner) + end + + it 'enables specific runner for project' do + within '.assigned-projects' do + click_on 'Disable' + end + + new_runner_project = page.find('.unassigned-projects') + + expect(new_runner_project).to have_content(@project1.path) + end + end end describe 'runners registration token' do diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 53b4f027117..203e55a36f2 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -26,7 +26,8 @@ describe "Container Registry" do end context 'when there are tags' do - it { expect(page).to have_content(tag_name)} + it { expect(page).to have_content(tag_name) } + it { expect(page).to have_content('d7a513a66') } end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 40fea5211e9..7fb28f4174b 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -20,7 +20,7 @@ feature 'Environments', feature: true do context 'without environments' do scenario 'does show no environments' do - expect(page).to have_content('No environments to show') + expect(page).to have_content('You don\'t have any environments right now.') end end @@ -61,7 +61,7 @@ feature 'Environments', feature: true do context 'without deployments' do scenario 'does show no deployments' do - expect(page).to have_content('No deployments for') + expect(page).to have_content('You don\'t have any deployments right now.') end end @@ -108,7 +108,7 @@ feature 'Environments', feature: true do end scenario 'does create a new pipeline' do - expect(page).to have_content('production') + expect(page).to have_content('Production') end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index c3cb3379440..5065dfb849c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -22,7 +22,7 @@ describe 'Issues', feature: true do before do visit edit_namespace_project_issue_path(project.namespace, project, issue) - click_button "Go full screen" + find('.js-zen-enter').click end it 'should open new issue popup' do diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 51be81d634c..01e90618a98 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'list of badges' do - include Select2Helper - background do user = create(:user) project = create(:project) @@ -24,7 +22,11 @@ feature 'list of badges' do end scenario 'user changes current ref on badges list page', js: true do - select2('improve/awesome', from: '#ref') + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'improve/awesome' + end expect(page).to have_content 'badges/improve/awesome/build.svg' end diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb new file mode 100644 index 00000000000..d516e8ce55a --- /dev/null +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a .gitlab-ci.yml file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitlab-ci.yml') + end + + scenario 'user can see .gitlab-ci.yml dropdown' do + expect(page).to have_css('.gitlab-ci-yml-selector') + end + + scenario 'user can pick a template from the dropdown', js: true do + find('.js-gitlab-ci-yml-selector').click + wait_for_ajax + within '.gitlab-ci-yml-selector' do + find('.dropdown-input-field').set('jekyll') + find('.dropdown-content li', text: 'jekyll').click + end + wait_for_ajax + + expect(page).to have_content('This file is a template, and might need editing before it works on your project') + expect(page).to have_content('jekyll build -d test') + end +end diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json index 1b6008e2872..8d1b874c29b 100644 --- a/spec/fixtures/container_registry/tag_manifest.json +++ b/spec/fixtures/container_registry/tag_manifest.json @@ -1 +1,16 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]} +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/octet-stream", + "size": 1145, + "digest": "sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2319870, + "digest": "sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d" + } + ] +} diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index f6c1005d265..bb28866f010 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -174,51 +174,6 @@ describe ApplicationHelper do end end - describe 'grouped_options_refs' do - let(:options) { helper.grouped_options_refs } - let(:project) { create(:project) } - - before do - assign(:project, project) - - # Override Rails' grouped_options_for_select helper to just return the - # first argument (`options`), since it's easier to work with than the - # generated HTML. - allow(helper).to receive(:grouped_options_for_select). - and_wrap_original { |_, *args| args.first } - end - - it 'includes a list of branch names' do - expect(options[0][0]).to eq('Branches') - expect(options[0][1]).to include('master', 'feature') - end - - it 'includes a list of tag names' do - expect(options[1][0]).to eq('Tags') - expect(options[1][1]).to include('v1.0.0', 'v1.1.0') - end - - it 'includes a specific commit ref if defined' do - # Must be an instance variable - ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8' - assign(:ref, ref) - - expect(options[2][0]).to eq('Commit') - expect(options[2][1]).to eq([ref]) - end - - it 'sorts tags in a natural order' do - # Stub repository.tag_names to make sure we get some valid testing data - expect(project.repository).to receive(:tag_names). - and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿', - 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2']) - - expect(options[1][1]). - to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10', - 'v1.0.9', 'v1.0.9a']) - end - end - describe 'simple_sanitize' do let(:a_tag) { '<a href="#">Foo</a>' } diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index ea27f36e9b5..71f0c1076c5 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -1,3 +1,4 @@ +#= require lib/text_utility #= require issue describe 'Issue', -> @@ -38,7 +39,7 @@ describe 'reopen/close issue', -> expect(typeof $btnClose.prop('disabled')).toBe('undefined') $btnClose.trigger('click') - + expect($btnReopen).toBeVisible() expect($btnClose).toBeHidden() expect($('div.status-box-closed')).toBeVisible() @@ -50,7 +51,7 @@ describe 'reopen/close issue', -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://goesnowhere.nothing/whereami') req.success saved: false - + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') $btnClose.attr('href','http://goesnowhere.nothing/whereami') @@ -59,7 +60,7 @@ describe 'reopen/close issue', -> expect(typeof $btnClose.prop('disabled')).toBe('undefined') $btnClose.trigger('click') - + expect($btnReopen).toBeHidden() expect($btnClose).toBeVisible() expect($('div.status-box-closed')).toBeHidden() @@ -73,7 +74,7 @@ describe 'reopen/close issue', -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://goesnowhere.nothing/whereami') req.error() - + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') $btnClose.attr('href','http://goesnowhere.nothing/whereami') @@ -82,7 +83,7 @@ describe 'reopen/close issue', -> expect(typeof $btnClose.prop('disabled')).toBe('undefined') $btnClose.trigger('click') - + expect($btnReopen).toBeHidden() expect($btnClose).toBeVisible() expect($('div.status-box-closed')).toBeHidden() @@ -105,4 +106,4 @@ describe 'reopen/close issue', -> expect($btnReopen).toBeHidden() expect($btnClose).toBeVisible() expect($('div.status-box-open')).toBeVisible() - expect($('div.status-box-closed')).toBeHidden()
\ No newline at end of file + expect($('div.status-box-closed')).toBeHidden() diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 0e6685f0ffb..b9e4a4eaf0e 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -132,11 +132,8 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do path = 'files/images/한글.png' escaped = Addressable::URI.escape(path) - # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) + # Stub this method so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) doc = filter(image(escaped)) expect(doc.at_css('img')['src']).to match '/raw/' diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index 88814bc474d..659facd6c19 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -17,12 +17,15 @@ describe Gitlab::Lfs::Router, lib: true do } end - let(:lfs_router_auth) { new_lfs_router(project, user) } - let(:lfs_router_noauth) { new_lfs_router(project, nil) } - let(:lfs_router_public_auth) { new_lfs_router(public_project, user) } - let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) } - let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) } - let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) } + let(:lfs_router_auth) { new_lfs_router(project, user: user) } + let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) } + let(:lfs_router_noauth) { new_lfs_router(project) } + let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) } + let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) } + let(:lfs_router_public_noauth) { new_lfs_router(public_project) } + let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) } + let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) } + let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) } let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } @@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do context 'with required headers' do before do + project.lfs_objects << lfs_object env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" end @@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do context 'when user has project access' do before do - project.lfs_objects << lfs_object project.team << [user, :master] end @@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) end end + + context 'when CI is authorized' do + it "responds with status 200" do + expect(lfs_router_ci_auth.try_call.first).to eq(200) + end + + it "responds with the file location" do + expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") + expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) + end + end end context 'without required headers' do @@ -134,143 +148,145 @@ describe Gitlab::Lfs::Router, lib: true do end describe 'download' do - describe 'when user is authenticated' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end - describe 'when user has download access' do + shared_examples 'an authorized requests' do + context 'when downloading an lfs object that is assigned to our project' do before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :reporter] + project.lfs_objects << lfs_object end - context 'when downloading an lfs object that is assigned to our project' do - before do - project.lfs_objects << lfs_object - end - - it 'responds with status 200 and href to download' do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + it 'responds with status 200 and href to download' do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => @auth } - } + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => auth } } - }]) - end + } + }]) end + end - context 'when downloading an lfs object that is assigned to other project' do - before do - public_project.lfs_objects << lfs_object - end + context 'when downloading an lfs object that is assigned to other project' do + before do + public_project.lfs_objects << lfs_object + end - it 'responds with status 200 and error message' do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + it 'responds with status 200 and error message' do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }]) - end + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) end + end - context 'when downloading a lfs object that does not exist' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end + context 'when downloading a lfs object that does not exist' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end - it "responds with status 200 and error message" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + it "responds with status 200 and error message" do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }]) - end + expect(response_body).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) end + end - context 'when downloading one new and one existing lfs object' do - before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, - { 'oid' => sample_oid, - 'size' => sample_size - } - ] - }.to_json - env['rack.input'] = StringIO.new(body) - project.lfs_objects << lfs_object - end + context 'when downloading one new and one existing lfs object' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + }.to_json + env['rack.input'] = StringIO.new(body) + project.lfs_objects << lfs_object + end - it "responds with status 200 with upload hypermedia link for the new object" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - response_body = ActiveSupport::JSON.decode(response.last.first) + it "responds with status 200 with upload hypermedia link for the new object" do + response = router.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }, - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => @auth } - } + expect(response_body).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }, + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => auth } } - }]) - end + } + }]) end end + end + + context 'when user is authenticated' do + let(:auth) { authorize(user) } + + before do + env["HTTP_AUTHORIZATION"] = auth + project.team << [user, role] + end + + it_behaves_like 'an authorized requests' do + let(:role) { :reporter } + let(:router) { lfs_router_auth } + end context 'when user does is not member of the project' do - before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :guest] - end + let(:role) { :guest } it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) @@ -278,11 +294,7 @@ describe Gitlab::Lfs::Router, lib: true do end context 'when user does not have download access' do - before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :guest] - end + let(:role) { :guest } it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) @@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do end end - context 'when user is not authenticated' do + context 'when CI is authorized' do + let(:auth) { 'gitlab-ci-token:password' } + before do - body = { 'operation' => 'download', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }], + env["HTTP_AUTHORIZATION"] = auth + end - }.to_json - env['rack.input'] = StringIO.new(body) + it_behaves_like 'an authorized requests' do + let(:router) { lfs_router_ci_auth } end + end + context 'when user is not authenticated' do describe 'is accessing public project' do before do public_project.lfs_objects << lfs_object @@ -338,17 +351,17 @@ describe Gitlab::Lfs::Router, lib: true do end describe 'upload' do - describe 'when user is authenticated' do - before do - body = { 'operation' => 'upload', - 'objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end + before do + body = { 'operation' => 'upload', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + describe 'when request is authenticated' do describe 'when user has project push access' do before do @auth = authorize(user) @@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do expect(lfs_router_auth.try_call.first).to eq(403) end end - end - context 'when user is not authenticated' do - before do - env['rack.input'] = StringIO.new( - { 'objects' => [], 'operation' => 'upload' }.to_json - ) + context 'when CI is authorized' do + it 'responds with 401' do + expect(lfs_router_ci_auth.try_call.first).to eq(401) + end end + end + context 'when user is not authenticated' do context 'when user has push access' do before do project.team << [user, :master] @@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do end end end + + context 'when CI is authorized' do + let(:auth) { 'gitlab-ci-token:password' } + + before do + env["HTTP_AUTHORIZATION"] = auth + end + + it "responds with status 403" do + expect(lfs_router_public_ci_auth.try_call.first).to eq(401) + end + end end describe 'unsupported' do @@ -490,13 +515,68 @@ describe Gitlab::Lfs::Router, lib: true do env['REQUEST_METHOD'] = 'PUT' end - describe 'to one project' do - describe 'when user has push access to the project' do + shared_examples 'unauthorized' do + context 'and request is sent by gitlab-workhorse to authorize the request' do before do - project.team << [user, :master] + header_for_upload_authorize(router.project) + end + + it 'responds with status 401' do + expect(router.try_call.first).to eq(401) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(router.project) + end + + it 'responds with status 401' do + expect(router.try_call.first).to eq(401) + end + end + + context 'and request is sent with a malformed headers' do + before do + env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" + env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" + end + + it 'does not recognize it as a valid lfs command' do + expect(router.try_call).to eq(nil) + end + end + end + + shared_examples 'forbidden' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + header_for_upload_authorize(router.project) + end + + it 'responds with 403' do + expect(router.try_call.first).to eq(403) + end + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + headers_for_upload_finalize(router.project) + end + + it 'responds with 403' do + expect(router.try_call.first).to eq(403) end + end + end + + describe 'to one project' do + describe 'when user is authenticated' do + describe 'when user has push access to the project' do + before do + project.team << [user, :developer] + end - describe 'when user is authenticated' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(project) @@ -524,100 +604,35 @@ describe Gitlab::Lfs::Router, lib: true do end end - describe 'when user is unauthenticated' do - let(:lfs_router_noauth) { new_lfs_router(project, nil) } + describe 'and user does not have push access' do + let(:router) { lfs_router_auth } - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end - - it 'responds with status 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end - - it 'responds with status 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent with a malformed headers' do - before do - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" - env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" - end - - it 'does not recognize it as a valid lfs command' do - expect(lfs_router_noauth.try_call).to eq(nil) - end - end + it_behaves_like 'forbidden' end end - describe 'and user does not have push access' do - describe 'when user is authenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end - - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end - - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - end + context 'when CI is authenticated' do + let(:router) { lfs_router_ci_auth } - describe 'when user is unauthenticated' do - let(:lfs_router_noauth) { new_lfs_router(project, nil) } - - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(project) - end + it_behaves_like 'unauthorized' + end - it 'responds with 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end + context 'for unauthenticated' do + let(:router) { new_lfs_router(project) } - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(project) - end - - it 'responds with 401' do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - end + it_behaves_like 'unauthorized' end end - describe "to a forked project" do + describe 'to a forked project' do let(:forked_project) { fork_project(public_project, user) } - describe 'when user has push access to the project' do - before do - forked_project.team << [user_two, :master] - end + describe 'when user is authenticated' do + describe 'when user has push access to the project' do + before do + forked_project.team << [user_two, :developer] + end - describe 'when user is authenticated' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(forked_project) @@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do end end - describe 'when user is unauthenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end - - it 'responds with status 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end + describe 'and user does not have push access' do + let(:router) { lfs_router_forked_auth } - it 'responds with status 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end + it_behaves_like 'forbidden' end end - describe 'and user does not have push access' do - describe 'when user is authenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end + context 'when CI is authenticated' do + let(:router) { lfs_router_forked_ci_auth } - it 'responds with 403' do - expect(lfs_router_forked_auth.try_call.first).to eq(403) - end - end - - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end - - it 'responds with 403' do - expect(lfs_router_forked_auth.try_call.first).to eq(403) - end - end - end - - describe 'when user is unauthenticated' do - context 'and request is sent by gitlab-workhorse to authorize the request' do - before do - header_for_upload_authorize(forked_project) - end - - it 'responds with 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end + it_behaves_like 'unauthorized' + end - context 'and request is sent by gitlab-workhorse to finalize the upload' do - before do - headers_for_upload_finalize(forked_project) - end + context 'for unauthenticated' do + let(:router) { lfs_router_forked_noauth } - it 'responds with 401' do - expect(lfs_router_forked_noauth.try_call.first).to eq(401) - end - end - end + it_behaves_like 'unauthorized' end describe 'and second project not related to fork or a source project' do let(:second_project) { create(:project) } - let(:lfs_router_second_project) { new_lfs_router(second_project, user) } + let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) } before do public_project.lfs_objects << lfs_object @@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) end - def new_lfs_router(project, user) - Gitlab::Lfs::Router.new(project, user, request) + def new_lfs_router(project, user: nil, ci: false) + Gitlab::Lfs::Router.new(project, user, ci, request) end def header_for_upload_authorize(project) diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_spec.rb index 72baa516cc4..bc0ec9325cc 100644 --- a/spec/lib/gitlab/gitignore_spec.rb +++ b/spec/lib/gitlab/template/gitignore_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Gitlab::Gitignore do - subject { Gitlab::Gitignore } +describe Gitlab::Template::Gitignore do + subject { described_class } describe '.all' do it 'strips the gitignore suffix' do @@ -24,7 +24,7 @@ describe Gitlab::Gitignore do it 'returns the Gitignore object of a valid file' do ruby = subject.find('Ruby') - expect(ruby).to be_a Gitlab::Gitignore + expect(ruby).to be_a Gitlab::Template::Gitignore expect(ruby.name).to eq('Ruby') end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 1e6eb20ab39..ae55a01ebea 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -401,23 +401,56 @@ describe Notify do end describe 'project access requested' do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:project_member) do - project.request_access(user) - project.members.request.find_by(user_id: user.id) + context 'for a project in a user namespace' do + let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } } + let(:user) { create(:user) } + let(:project_member) do + project.request_access(user) + project.members.request.find_by(user_id: user.id) + end + subject { Notify.member_access_requested_email('project', project_member.id) } + + 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 'contains all the useful information' do + to_emails = subject.header[:to].addrs + expect(to_emails.size).to eq(1) + expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) + + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ + is_expected.to have_body_text /#{project_member.human_access}/ + end end - subject { Notify.member_access_requested_email('project', project_member.id) } - 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" + context 'for a project in a group' do + let(:group_owner) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } + let(:project) { create(:project, namespace: group) } + let(:user) { create(:user) } + let(:project_member) do + project.request_access(user) + project.members.request.find_by(user_id: user.id) + end + subject { Notify.member_access_requested_email('project', project_member.id) } - it 'contains all the useful information' do - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + 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 'contains all the useful information' do + to_emails = subject.header[:to].addrs + expect(to_emails.size).to eq(1) + expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) + + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ + is_expected.to have_body_text /#{project_member.human_access}/ + end end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 5d1fa8226e5..8154001cf46 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -2,7 +2,12 @@ require 'spec_helper' describe Ci::Build, models: true do let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.id) + end + let(:build) { create(:ci_build, pipeline: pipeline) } it { is_expected.to validate_presence_of :ref } @@ -36,32 +41,44 @@ describe Ci::Build, models: true do subject { build.ignored? } context 'if build is not allowed to fail' do - before { build.allow_failure = false } + before do + build.allow_failure = false + end context 'and build.status is success' do - before { build.status = 'success' } + before do + build.status = 'success' + end it { is_expected.to be_falsey } end context 'and build.status is failed' do - before { build.status = 'failed' } + before do + build.status = 'failed' + end it { is_expected.to be_falsey } end end context 'if build is allowed to fail' do - before { build.allow_failure = true } + before do + build.allow_failure = true + end context 'and build.status is success' do - before { build.status = 'success' } + before do + build.status = 'success' + end it { is_expected.to be_falsey } end context 'and build.status is failed' do - before { build.status = 'failed' } + before do + build.status = 'failed' + end it { is_expected.to be_truthy } end @@ -75,7 +92,9 @@ describe Ci::Build, models: true do context 'if build.trace contains text' do let(:text) { 'example output' } - before { build.trace = text } + before do + build.trace = text + end it { is_expected.to include(text) } it { expect(subject.length).to be >= text.length } @@ -188,7 +207,9 @@ describe Ci::Build, models: true do ] end - before { build.update_attributes(stage: 'stage') } + before do + build.update_attributes(stage: 'stage') + end it { is_expected.to eq(predefined_variables + yaml_variables) } @@ -199,7 +220,9 @@ describe Ci::Build, models: true do ] end - before { build.update_attributes(tag: true) } + before do + build.update_attributes(tag: true) + end it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) } end @@ -257,57 +280,6 @@ describe Ci::Build, models: true do end end - describe '#can_be_served?' do - let(:runner) { create(:ci_runner) } - - before { build.project.runners << runner } - - context 'when runner does not have tags' do - it 'can handle builds without tags' do - expect(build.can_be_served?(runner)).to be_truthy - end - - it 'cannot handle build with tags' do - build.tag_list = ['aa'] - expect(build.can_be_served?(runner)).to be_falsey - end - end - - context 'when runner has tags' do - before { runner.tag_list = ['bb', 'cc'] } - - shared_examples 'tagged build picker' do - it 'can handle build with matching tags' do - build.tag_list = ['bb'] - expect(build.can_be_served?(runner)).to be_truthy - end - - it 'cannot handle build without matching tags' do - build.tag_list = ['aa'] - expect(build.can_be_served?(runner)).to be_falsey - end - end - - context 'when runner can pick untagged jobs' do - it 'can handle builds without tags' do - expect(build.can_be_served?(runner)).to be_truthy - end - - it_behaves_like 'tagged build picker' - end - - context 'when runner can not pick untagged jobs' do - before { runner.run_untagged = false } - - it 'can not handle builds without tags' do - expect(build.can_be_served?(runner)).to be_falsey - end - - it_behaves_like 'tagged build picker' - end - end - end - describe '#has_tags?' do context 'when build has tags' do subject { create(:ci_build, tag_list: ['tag']) } @@ -348,7 +320,7 @@ describe Ci::Build, models: true do end it 'that cannot handle build' do - expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false) + expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) is_expected.to be_falsey end @@ -360,7 +332,9 @@ describe Ci::Build, models: true do %w(pending).each do |state| context "if commit_status.status is #{state}" do - before { build.status = state } + before do + build.status = state + end it { is_expected.to be_truthy } @@ -379,7 +353,9 @@ describe Ci::Build, models: true do %w(success failed canceled running).each do |state| context "if commit_status.status is #{state}" do - before { build.status = state } + before do + build.status = state + end it { is_expected.to be_falsey } end @@ -390,7 +366,10 @@ describe Ci::Build, models: true do subject { build.artifacts? } context 'artifacts archive does not exist' do - before { build.update_attributes(artifacts_file: nil) } + before do + build.update_attributes(artifacts_file: nil) + end + it { is_expected.to be_falsy } end @@ -623,7 +602,9 @@ describe Ci::Build, models: true do let!(:build) { create(:ci_build, :trace, :success, :artifacts) } describe '#erase' do - before { build.erase(erased_by: user) } + before do + build.erase(erased_by: user) + end context 'erased by user' do let!(:user) { create(:user, username: 'eraser') } @@ -660,7 +641,9 @@ describe Ci::Build, models: true do end context 'build has been erased' do - before { build.erase } + before do + build.erase + end it { is_expected.to be true } end @@ -668,7 +651,9 @@ describe Ci::Build, models: true do context 'metadata and build trace are not available' do let!(:build) { create(:ci_build, :success, :artifacts) } - before { build.remove_artifacts_metadata! } + before do + build.remove_artifacts_metadata! + end describe '#erase' do it 'should not raise error' do @@ -678,4 +663,10 @@ describe Ci::Build, models: true do end end end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(build.commit).to eq project.commit + end + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 5d04d8ffcff..ef65eb99328 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -20,34 +20,36 @@ describe Ci::Runner, models: true do end describe '#display_name' do - it 'should return the description if it has a value' do + it 'returns the description if it has a value' do runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448' end - it 'should return the token if it does not have a description' do + it 'returns the token if it does not have a description' do runner = FactoryGirl.create(:ci_runner) expect(runner.display_name).to eq runner.description end - it 'should return the token if the description is an empty string' do + it 'returns the token if the description is an empty string' do runner = FactoryGirl.build(:ci_runner, description: '', token: 'token') expect(runner.display_name).to eq runner.token end end - describe :assign_to do + describe '#assign_to' do let!(:project) { FactoryGirl.create :empty_project } let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) } - before { shared_runner.assign_to(project) } + before do + shared_runner.assign_to(project) + end it { expect(shared_runner).to be_specific } it { expect(shared_runner.projects).to eq([project]) } it { expect(shared_runner.only_for?(project)).to be_truthy } end - describe :online do + describe '.online' do subject { Ci::Runner.online } before do @@ -58,60 +60,269 @@ describe Ci::Runner, models: true do it { is_expected.to eq([@runner2])} end - describe :online? do + describe '#online?' do let(:runner) { FactoryGirl.create(:ci_runner, :shared) } subject { runner.online? } context 'never contacted' do - before { runner.contacted_at = nil } + before do + runner.contacted_at = nil + end it { is_expected.to be_falsey } end context 'contacted long time ago time' do - before { runner.contacted_at = 1.year.ago } + before do + runner.contacted_at = 1.year.ago + end it { is_expected.to be_falsey } end context 'contacted 1s ago' do - before { runner.contacted_at = 1.second.ago } + before do + runner.contacted_at = 1.second.ago + end it { is_expected.to be_truthy } end end - describe :status do + describe '#can_pick?' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:runner) { create(:ci_runner) } + + before do + build.project.runners << runner + end + + context 'when runner does not have tags' do + it 'can handle builds without tags' do + expect(runner.can_pick?(build)).to be_truthy + end + + it 'cannot handle build with tags' do + build.tag_list = ['aa'] + + expect(runner.can_pick?(build)).to be_falsey + end + end + + context 'when runner has tags' do + before do + runner.tag_list = ['bb', 'cc'] + end + + shared_examples 'tagged build picker' do + it 'can handle build with matching tags' do + build.tag_list = ['bb'] + + expect(runner.can_pick?(build)).to be_truthy + end + + it 'cannot handle build without matching tags' do + build.tag_list = ['aa'] + + expect(runner.can_pick?(build)).to be_falsey + end + end + + context 'when runner can pick untagged jobs' do + it 'can handle builds without tags' do + expect(runner.can_pick?(build)).to be_truthy + end + + it_behaves_like 'tagged build picker' + end + + context 'when runner cannot pick untagged jobs' do + before do + runner.run_untagged = false + end + + it 'cannot handle builds without tags' do + expect(runner.can_pick?(build)).to be_falsey + end + + it_behaves_like 'tagged build picker' + end + end + + context 'when runner is locked' do + before do + runner.locked = true + end + + shared_examples 'locked build picker' do + context 'when runner cannot pick untagged jobs' do + before do + runner.run_untagged = false + end + + it 'cannot handle builds without tags' do + expect(runner.can_pick?(build)).to be_falsey + end + end + + context 'when having runner tags' do + before do + runner.tag_list = ['bb', 'cc'] + end + + it 'cannot handle it for builds without matching tags' do + build.tag_list = ['aa'] + + expect(runner.can_pick?(build)).to be_falsey + end + end + end + + context 'when serving the same project' do + it 'can handle it' do + expect(runner.can_pick?(build)).to be_truthy + end + + it_behaves_like 'locked build picker' + + context 'when having runner tags' do + before do + runner.tag_list = ['bb', 'cc'] + build.tag_list = ['bb'] + end + + it 'can handle it for matching tags' do + expect(runner.can_pick?(build)).to be_truthy + end + end + end + + context 'serving a different project' do + before do + runner.runner_projects.destroy_all + end + + it 'cannot handle it' do + expect(runner.can_pick?(build)).to be_falsey + end + + it_behaves_like 'locked build picker' + + context 'when having runner tags' do + before do + runner.tag_list = ['bb', 'cc'] + build.tag_list = ['bb'] + end + + it 'cannot handle it for matching tags' do + expect(runner.can_pick?(build)).to be_falsey + end + end + end + end + end + + describe '#status' do let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) } subject { runner.status } context 'never connected' do - before { runner.contacted_at = nil } + before do + runner.contacted_at = nil + end it { is_expected.to eq(:not_connected) } end context 'contacted 1s ago' do - before { runner.contacted_at = 1.second.ago } + before do + runner.contacted_at = 1.second.ago + end it { is_expected.to eq(:online) } end context 'contacted long time ago' do - before { runner.contacted_at = 1.year.ago } + before do + runner.contacted_at = 1.year.ago + end it { is_expected.to eq(:offline) } end context 'inactive' do - before { runner.active = false } + before do + runner.active = false + end it { is_expected.to eq(:paused) } end end + describe '.assignable_for' do + let(:runner) { create(:ci_runner) } + let(:project) { create(:project) } + let(:another_project) { create(:project) } + + before do + project.runners << runner + end + + context 'with shared runners' do + before do + runner.update(is_shared: true) + end + + context 'does not give owned runner' do + subject { Ci::Runner.assignable_for(project) } + + it { is_expected.to be_empty } + end + + context 'does not give shared runner' do + subject { Ci::Runner.assignable_for(another_project) } + + it { is_expected.to be_empty } + end + end + + context 'with unlocked runner' do + context 'does not give owned runner' do + subject { Ci::Runner.assignable_for(project) } + + it { is_expected.to be_empty } + end + + context 'does give a specific runner' do + subject { Ci::Runner.assignable_for(another_project) } + + it { is_expected.to contain_exactly(runner) } + end + end + + context 'with locked runner' do + before do + runner.update(locked: true) + end + + context 'does not give owned runner' do + subject { Ci::Runner.assignable_for(project) } + + it { is_expected.to be_empty } + end + + context 'does not give a locked runner' do + subject { Ci::Runner.assignable_for(another_project) } + + it { is_expected.to be_empty } + end + end + end + describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do runner = FactoryGirl.create(:ci_runner) diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index beca8708c9d..ba02d5fe977 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -207,4 +207,16 @@ eos expect(commit.participants).to include(note1.author, note2.author) end end + + describe '#uri_type' do + it 'returns the URI type at the given path' do + expect(commit.uri_type('files/html')).to be(:tree) + expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) + expect(commit.uri_type('files/js/application.js')).to be(:blob) + end + + it "returns nil if the path doesn't exists" do + expect(commit.uri_type('this/path/doesnt/exist')).to be_nil + end + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 8fb605fff8a..96397d7c8a9 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,8 +1,13 @@ require 'spec_helper' describe CommitStatus, models: true do - let(:pipeline) { FactoryGirl.create :ci_pipeline } - let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.id) + end + + let(:commit_status) { create(:commit_status, pipeline: pipeline) } it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:user) } @@ -13,7 +18,7 @@ describe CommitStatus, models: true do it { is_expected.to delegate_method(:sha).to(:pipeline) } it { is_expected.to delegate_method(:short_sha).to(:pipeline) } - + it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } it { is_expected.to respond_to :running? } @@ -116,7 +121,7 @@ describe CommitStatus, models: true do it { is_expected.to be > 0.0 } end end - + describe :latest do subject { CommitStatus.latest.order(:id) } @@ -198,4 +203,10 @@ describe CommitStatus, models: true do end end end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(commit_status.commit).to eq project.commit + end + end end diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb index 7e4ea0f2d66..a9f4ef9ee5e 100644 --- a/spec/models/concerns/participable_spec.rb +++ b/spec/models/concerns/participable_spec.rb @@ -37,6 +37,16 @@ describe Participable, models: true do expect(participants).to include(user3) end + it 'caches the raw list of participants' do + instance = model.new + user1 = build(:user) + + expect(instance).to receive(:raw_participants).once + + instance.participants(user1) + instance.participants(user1) + end + it 'supports attributes returning another Participable' do other_model = Class.new { include Participable } diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index ac85f340922..47e9253a10c 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -9,8 +9,8 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let(:pipeline) { create(:ci_pipeline, project: project)} - let(:build) { create(:ci_build, pipeline: pipeline) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } + let!(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do let(:query) { '' } @@ -23,6 +23,11 @@ describe API::API, api: true do expect(json_response).to be_an Array end + it 'returns correct values' do + expect(json_response).not_to be_empty + expect(json_response.first['commit']['id']).to eq project.commit.id + end + context 'filter project with one scope element' do let(:query) { 'scope=pending' } @@ -132,7 +137,7 @@ describe API::API, api: true do describe 'GET /projects/:id/builds/:build_id/trace' do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - + before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } context 'authorized user' do diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb deleted file mode 100644 index aab2d8c81b9..00000000000 --- a/spec/requests/api/gitignores_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -describe API::Gitignores, api: true do - include ApiHelpers - - describe 'Entity Gitignore' do - before { get api('/gitignores/Ruby') } - - it { expect(json_response['name']).to eq('Ruby') } - it { expect(json_response['content']).to include('*.gem') } - end - - describe 'Entity GitignoresList' do - before { get api('/gitignores') } - - it { expect(json_response.first['name']).not_to be_nil } - it { expect(json_response.first['content']).to be_nil } - end - - describe 'GET /gitignores' do - it 'returns a list of available license templates' do - get api('/gitignores') - - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.size).to be > 15 - end - end -end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 73ae8ef631c..b4c826522a5 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -187,14 +187,16 @@ describe API::Runners, api: true do update_runner(shared_runner.id, admin, description: "#{description}_updated", active: !active, tag_list: ['ruby2.1', 'pgsql', 'mysql'], - run_untagged: 'false') + run_untagged: 'false', + locked: 'true') shared_runner.reload expect(response.status).to eq(200) expect(shared_runner.description).to eq("#{description}_updated") expect(shared_runner.active).to eq(!active) expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') - expect(shared_runner.run_untagged?).to be false + expect(shared_runner.run_untagged?).to be(false) + expect(shared_runner.locked?).to be(true) end end @@ -360,11 +362,13 @@ describe API::Runners, api: true do describe 'POST /projects/:id/runners' do context 'authorized user' do - it 'should enable specific runner' do - specific_runner2 = create(:ci_runner).tap do |runner| + let(:specific_runner2) do + create(:ci_runner).tap do |runner| create(:ci_runner_project, runner: runner, project: project2) end + end + it 'should enable specific runner' do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id end.to change{ project.runners.count }.by(+1) @@ -375,7 +379,17 @@ describe API::Runners, api: true do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id end.to change{ project.runners.count }.by(0) - expect(response.status).to eq(201) + expect(response.status).to eq(409) + end + + it 'should not enable locked runner' do + specific_runner2.update(locked: true) + + expect do + post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id + end.to change{ project.runners.count }.by(0) + + expect(response.status).to eq(403) end it 'should not enable shared runner' do diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb new file mode 100644 index 00000000000..a6d5ade3013 --- /dev/null +++ b/spec/requests/api/templates_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe API::Templates, api: true do + include ApiHelpers + + describe 'the Template Entity' do + before { get api('/gitignores/Ruby') } + + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end + + describe 'the TemplateList Entity' do + before { get api('/gitignores') } + + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } + end + + context 'requesting gitignores' do + describe 'GET /gitignores' do + it 'returns a list of available gitignore templates' do + get api('/gitignores') + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end + end + end + + context 'requesting gitlab-ci-ymls' do + describe 'GET /gitlab_ci_ymls' do + it 'returns a list of available gitlab_ci_ymls' do + get api('/gitlab_ci_ymls') + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).not_to be_nil + end + end + end + + describe 'GET /gitlab_ci_ymls/Ruby' do + it 'adds a disclaimer on the top' do + get api('/gitlab_ci_ymls/Ruby') + + expect(response.status).to eq(200) + expect(json_response['content']).to start_with("# This file is a template,") + end + end +end diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index 1abd87d7d33..b5e1fdb8ded 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -9,7 +9,7 @@ describe MergeWorker do before do source_project.team << [author, :master] - source_project.repository.expire_branch_names + source_project.repository.expire_branches_cache end it 'clears cache of source repo after removing source branch' do diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index a8368751267..f6b286cea98 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -2,7 +2,7 @@ *.apk *.ap_ -# Files for the Dalvik VM +# Files for the ART/Dalvik VM *.dex # Java class files @@ -34,6 +34,7 @@ captures/ # Intellij *.iml +.idea/workspace.xml # Keystore files *.jks diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index b8bd0267bdf..4581ef2eeef 100644 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore @@ -15,6 +15,7 @@ # Fortran module files *.mod +*.smod # Compiled Static libraries *.lai diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore index b558e9afa6d..0cc7e4b5275 100644 --- a/vendor/gitignore/CMake.gitignore +++ b/vendor/gitignore/CMake.gitignore @@ -4,3 +4,4 @@ CMakeScripts Makefile cmake_install.cmake install_manifest.txt +CTestTestfile.cmake diff --git a/vendor/gitignore/D.gitignore b/vendor/gitignore/D.gitignore index b4433f8a512..74b926fc901 100644 --- a/vendor/gitignore/D.gitignore +++ b/vendor/gitignore/D.gitignore @@ -18,3 +18,7 @@ .dub docs.json __dummy.html +docs/ + +# Code coverage +*.lst diff --git a/vendor/gitignore/Global/Bazaar.gitignore b/vendor/gitignore/Global/Bazaar.gitignore new file mode 100644 index 00000000000..3cbbcbd11ec --- /dev/null +++ b/vendor/gitignore/Global/Bazaar.gitignore @@ -0,0 +1,2 @@ +.bzr/ +.bzrignore diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/OSX.gitignore index 660b31353e8..5972fe50f66 100644 --- a/vendor/gitignore/Global/OSX.gitignore +++ b/vendor/gitignore/Global/OSX.gitignore @@ -1,4 +1,4 @@ -.DS_Store +*.DS_Store .AppleDouble .LSOverride @@ -15,6 +15,7 @@ Icon .TemporaryItems .Trashes .VolumeIcon.icns +.com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB diff --git a/vendor/gitignore/Global/README.md b/vendor/gitignore/Global/README.md new file mode 100644 index 00000000000..06b6649bd9a --- /dev/null +++ b/vendor/gitignore/Global/README.md @@ -0,0 +1,10 @@ +## Globally Useful gitignores + +This directory contains globally useful gitignores, +e.g. OS-specific and editor specific. + +For more on global gitignores: +<https://help.github.com/articles/ignoring-files/#create-a-global-gitignore> + +And a good blog post about 'em: +<http://augustl.com/blog/2009/global_gitignores> diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore index 1d4e6137591..69c8c2b29ce 100644 --- a/vendor/gitignore/Global/SublimeText.gitignore +++ b/vendor/gitignore/Global/SublimeText.gitignore @@ -12,3 +12,16 @@ # sftp configuration file sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore index 096abdd90b3..a4ee41ab62b 100644 --- a/vendor/gitignore/Haskell.gitignore +++ b/vendor/gitignore/Haskell.gitignore @@ -16,3 +16,4 @@ cabal.sandbox.config *.hp *.eventlog .stack-work/ +cabal.project.local diff --git a/vendor/gitignore/Julia.gitignore b/vendor/gitignore/Julia.gitignore new file mode 100644 index 00000000000..381e0b6d252 --- /dev/null +++ b/vendor/gitignore/Julia.gitignore @@ -0,0 +1,4 @@ +*.jl.cov +*.jl.*.cov +*.jl.mem +deps/deps.jl diff --git a/vendor/gitignore/LICENSE b/vendor/gitignore/LICENSE new file mode 100644 index 00000000000..b8a103ac9b1 --- /dev/null +++ b/vendor/gitignore/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore index c491fa2bc6f..1cd717b6921 100644 --- a/vendor/gitignore/Laravel.gitignore +++ b/vendor/gitignore/Laravel.gitignore @@ -7,7 +7,6 @@ app/storage/ # Laravel 5 & Lumen specific bootstrap/cache/ -storage/ .env.*.php .env.php .env diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore index 3020bc327a7..86f21d8e0ff 100644 --- a/vendor/gitignore/Objective-C.gitignore +++ b/vendor/gitignore/Objective-C.gitignore @@ -24,6 +24,8 @@ xcuserdata/ ## Obj-C/Swift specific *.hmap *.ipa +*.dSYM.zip +*.dSYM # CocoaPods # @@ -49,3 +51,10 @@ Carthage/Build fastlane/report.xml fastlane/screenshots + +#Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore index fa24b2efee8..c7659c24f38 100644 --- a/vendor/gitignore/Qt.gitignore +++ b/vendor/gitignore/Qt.gitignore @@ -34,5 +34,5 @@ Makefile* *.qmlproject.user.* # QtCtreator CMake -CMakeLists.txt.user +CMakeLists.txt.user* diff --git a/vendor/gitignore/README.md b/vendor/gitignore/README.md deleted file mode 100644 index 43131e815cc..00000000000 --- a/vendor/gitignore/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# .gitignore templates - -This directory contains language-specific .gitignore templates that are used by GitLab. - -These files were automatically pulled from [this repository](https://github.com/github/gitignore). -Please submit pull requests to that repository. There is no need to edit the files in this directory. - -## Bulk Update - -To update this directory with the latest changes in the repository, run: - -```sh -bundle exec rake gitlab:update_gitignore -``` diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore index 2121e0a8038..d8c256c1925 100644 --- a/vendor/gitignore/Rails.gitignore +++ b/vendor/gitignore/Rails.gitignore @@ -16,6 +16,10 @@ pickle-email-*.html config/initializers/secret_token.rb config/secrets.yml +# dotenv +# TODO Comment out this rule if environment variables can be committed +.env + ## Environment normalization: /.bundle /vendor/bundle diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore index 8a29fa52af4..2c22487b5e3 100644 --- a/vendor/gitignore/Swift.gitignore +++ b/vendor/gitignore/Swift.gitignore @@ -24,6 +24,8 @@ xcuserdata/ ## Obj-C/Swift specific *.hmap *.ipa +*.dSYM.zip +*.dSYM ## Playgrounds timeline.xctimeline diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore index 75b1186b0af..be0e4913c3a 100644 --- a/vendor/gitignore/UnrealEngine.gitignore +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -37,6 +37,7 @@ *.suo *.opensdf *.sdf +*.VC.db *.VC.opendb # Precompiled Assets diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index f1e3d20e056..67acbf42f5e 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -42,6 +42,7 @@ dlldata.c # DNX project.lock.json +project.fragment.lock.json artifacts/ *_i.c diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml new file mode 100644 index 00000000000..396d3f1b042 --- /dev/null +++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml @@ -0,0 +1,7 @@ +# Official docker image. +image: docker:latest + +build: + stage: build + script: + - docker build -t test . diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml new file mode 100644 index 00000000000..0b329aaf1c4 --- /dev/null +++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml @@ -0,0 +1,18 @@ +# This template uses the non default language docker image +# The image already has Hex installed. You might want to consider to use `elixir:latest` +image: trenpixster/elixir:latest + +# Pic zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +services: + - mysql:latest + - redis:latest + - postgres:latest + +before_script: + - mix deps.get + +mix: + script: + - mix test diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE new file mode 100644 index 00000000000..80f7b87b6c0 --- /dev/null +++ b/vendor/gitlab-ci-yml/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 GitLab.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml new file mode 100644 index 00000000000..e5bce3503f3 --- /dev/null +++ b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml @@ -0,0 +1,27 @@ +# Official framework image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/node/tags/ +image: node:latest + +# Pick zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +services: + - mysql:latest + - redis:latest + - postgres:latest + +# This folder is cached between builds +# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +cache: + paths: + - node_modules/ + +test_async: + script: + - npm install + - node ./specs/start.js ./specs/async.spec.js + +test_db: + script: + - npm install + - node ./specs/start.js ./specs/db-postgres.spec.js diff --git a/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml new file mode 100644 index 00000000000..7fcc0b436b5 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml @@ -0,0 +1,16 @@ +# Full project: https://gitlab.com/pages/brunch +image: node:4.2.2 + +pages: + cache: + paths: + - node_modules/ + + script: + - npm install -g brunch + - brunch build --production + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml new file mode 100644 index 00000000000..791afdd23f1 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml @@ -0,0 +1,13 @@ +# Full project: https://gitlab.com/pages/doxygen +image: alpine + +pages: + script: + - apk update && apk add doxygen + - doxygen doxygen/Doxyfile + - mv doxygen/documentation/html/ public/ + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml new file mode 100644 index 00000000000..dd3ef149668 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml @@ -0,0 +1,16 @@ +# Full project: https://gitlab.com/pages/harp +image: node:4.2.2 + +pages: + cache: + paths: + - node_modules + + script: + - npm install -g harp + - harp compile ./ public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml new file mode 100644 index 00000000000..b468d79bcad --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml @@ -0,0 +1,25 @@ +# Full project: https://gitlab.com/pages/hexo +image: python:2.7 + +cache: + paths: + - vendor/ + +test: + stage: test + script: + - pip install hyde + - hyde gen + except: + - master + +pages: + stage: deploy + script: + - pip install hyde + - hyde gen -d public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml new file mode 100644 index 00000000000..249a168aa33 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml @@ -0,0 +1,12 @@ +# Full project: https://gitlab.com/pages/plain-html +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml new file mode 100644 index 00000000000..45df6975259 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml @@ -0,0 +1,11 @@ +# Full project: https://gitlab.com/pages/hugo +image: publysher/hugo + +pages: + script: + - hugo + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml new file mode 100644 index 00000000000..f5b40f2b9f1 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml @@ -0,0 +1,25 @@ +# Full project: https://gitlab.com/pages/hyde +image: python:2.7 + +cache: + paths: + - vendor/ + +test: + stage: test + script: + - pip install hyde + - hyde gen + except: + - master + +pages: + stage: deploy + script: + - pip install hyde + - hyde gen -d public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml new file mode 100644 index 00000000000..36918fc005a --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml @@ -0,0 +1,24 @@ +# Full project: https://gitlab.com/pages/jekyll +image: ruby:2.3 + +test: + stage: test + script: + - gem install jekyll + - jekyll build -d test + artifacts: + paths: + - test + except: + - master + +pages: + stage: deploy + script: + - gem install jekyll + - jekyll build -d public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml new file mode 100644 index 00000000000..c5c44a5d86c --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml @@ -0,0 +1,12 @@ +# Full project: https://gitlab.com/pages/hyde +image: python:2.7 + +pages: + script: + - pip install lektor + - lektor build --output-path public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml new file mode 100644 index 00000000000..50e8b7ccd46 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml @@ -0,0 +1,17 @@ +# Full project: https://gitlab.com/pages/metalsmith +image: node:4.2.2 + +pages: + cache: + paths: + - node_modules/ + + script: + - npm install -g metalsmith + - npm install + - make build + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml new file mode 100644 index 00000000000..9f4cc0574d6 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml @@ -0,0 +1,27 @@ +# Full project: https://gitlab.com/pages/middleman +image: ruby:2.3 + +cache: + paths: + - vendor + +test: + script: + - apt-get update -yqqq + - apt-get install -y nodejs + - bundle install --path vendor + - bundle exec middleman build + except: + - master + +pages: + script: + - apt-get update -yqqq + - apt-get install -y nodejs + - bundle install --path vendor + - bundle exec middleman build + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml new file mode 100644 index 00000000000..b469b316ba5 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml @@ -0,0 +1,12 @@ +# Full project: https://gitlab.com/pages/nanoc +image: ruby:2.3 + +pages: + script: + - bundle install -j4 + - nanoc + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml new file mode 100644 index 00000000000..4762ec9acfd --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml @@ -0,0 +1,15 @@ +# Full project: https://gitlab.com/pages/octopress +image: ruby:2.3 + +pages: + script: + - apt-get update -qq && apt-get install -qq nodejs + - bundle install -j4 + - bundle exec rake generate + - mv public .public + - mv .public/octopress public + artifacts: + paths: + - public + only: + - master diff --git a/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml new file mode 100644 index 00000000000..c5f3154f587 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml @@ -0,0 +1,10 @@ +# Full project: https://gitlab.com/pages/pelican +image: python:2.7-alpine + +pages: + script: + - pip install -r requirements.txt + - pelican -s publishconf.py + artifacts: + paths: + - public/ diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml new file mode 100644 index 00000000000..78f3e39949f --- /dev/null +++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml @@ -0,0 +1,30 @@ +# Official language image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/ruby/tags/ +image: "ruby:2.3" + +# Pick zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +services: + - mysql:latest + - redis:latest + - postgres:latest + +# This is a basic example for a gem or script which doesn't use +# services such as redis or postgres +before_script: + - gem install bundler # Bundler is not installed with the image + - bundle install -j $(nproc) # Install dependencies + +rubocop: + script: + - rubocop + +rspec: + script: + - rspec spec + +rails: + script: + - rake db:migrate + - rspec spec |