diff options
author | Phil Hughes <me@iamphill.com> | 2017-09-05 09:21:36 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-09-05 09:21:36 +0100 |
commit | 365adcf6a3bd684abfa9aac3b47e53c1eb41bac6 (patch) | |
tree | 7787c1190dc1393a5b91c5e882e77a6df26f2b2d | |
parent | 78b0c35ce71b2e6083d655c47977766bf56fbe72 (diff) | |
parent | d4ec63186b106bbee4bde890b7c30b14a7748339 (diff) | |
download | gitlab-ce-365adcf6a3bd684abfa9aac3b47e53c1eb41bac6.tar.gz |
Merge branch 'master' into breadcrumbs-improvements
62 files changed, 1817 insertions, 425 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab9627d4ab7..aa7659d1b41 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -125,6 +125,7 @@ stages: - export KNAPSACK_GENERATE_REPORT=true - export CACHE_CLASSES=true - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} + - scripts/gitaly-test-spawn - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js new file mode 100644 index 00000000000..800ca05cd11 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -0,0 +1,61 @@ +import Cookies from 'js-cookie'; +import _ from 'underscore'; +import { + getCookieName, + getSelector, + hidePopover, + setupDismissButton, + mouseenter, + mouseleave, +} from './feature_highlight_helper'; + +export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => { + const $selector = $(getSelector(id)); + const $parent = $selector.parent(); + const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); + const hideOnScroll = hidePopover.bind($selector); + const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); + + $selector + // Setup popover + .data('content', $popoverContent.prop('outerHTML')) + .popover({ + html: true, + // Override the existing template to add custom CSS classes + template: ` + <div class="popover feature-highlight-popover" role="tooltip"> + <div class="arrow"></div> + <div class="popover-content"></div> + </div> + `, + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave) + .on('inserted.bs.popover', setupDismissButton) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll); + }) + .on('hide.bs.popover', () => { + window.removeEventListener('scroll', hideOnScroll); + }) + // Display feature highlight + .removeAttr('disabled'); +}; + +export const shouldHighlightFeature = (id) => { + const element = document.querySelector(getSelector(id)); + const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true'; + + return element && !previouslyDismissed; +}; + +export const highlightFeatures = (highlightOrder) => { + const featureId = highlightOrder.find(shouldHighlightFeature); + + if (featureId) { + setupFeatureHighlightPopover(featureId); + return true; + } + + return false; +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js new file mode 100644 index 00000000000..9f741355cd7 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -0,0 +1,57 @@ +import Cookies from 'js-cookie'; + +export const getCookieName = cookieId => `feature-highlighted-${cookieId}`; +export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; + +export const showPopover = function showPopover() { + if (this.hasClass('js-popover-show')) { + return false; + } + this.popover('show'); + this.addClass('disable-animation js-popover-show'); + + return true; +}; + +export const hidePopover = function hidePopover() { + if (!this.hasClass('js-popover-show')) { + return false; + } + this.popover('hide'); + this.removeClass('disable-animation js-popover-show'); + + return true; +}; + +export const dismiss = function dismiss(cookieId) { + Cookies.set(getCookieName(cookieId), true); + hidePopover.call(this); + this.hide(); +}; + +export const mouseleave = function mouseleave() { + if (!$('.popover:hover').length > 0) { + const $featureHighlight = $(this); + hidePopover.call($featureHighlight); + } +}; + +export const mouseenter = function mouseenter() { + const $featureHighlight = $(this); + + const showedPopover = showPopover.call($featureHighlight); + if (showedPopover) { + $('.popover') + .on('mouseleave', mouseleave.bind($featureHighlight)); + } +}; + +export const setupDismissButton = function setupDismissButton() { + const popoverId = this.getAttribute('aria-describedby'); + const cookieId = this.dataset.highlight; + const $popover = $(this); + const dismissWrapper = dismiss.bind($popover, cookieId); + + $(`#${popoverId} .dismiss-feature-highlight`) + .on('click', dismissWrapper); +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js new file mode 100644 index 00000000000..fd48f2e87cc --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js @@ -0,0 +1,12 @@ +import { highlightFeatures } from './feature_highlight'; +import bp from '../breakpoints'; + +const highlightOrder = ['issue-boards']; + +export default function domContentLoaded(order) { + if (bp.getBreakpointSize() === 'lg') { + highlightFeatures(order); + } +} + +document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder)); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 928394f4fae..66f8cbd7139 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -102,6 +102,7 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; +import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b2b3297e880..c0524bf6aa3 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -51,3 +51,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive-tables"; +@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index b4a6b214e98..82350c36df0 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -46,6 +46,15 @@ } } +@mixin btn-svg { + svg { + height: 15px; + width: 15px; + position: relative; + top: 2px; + } +} + @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -123,6 +132,7 @@ .btn { @include btn-default; @include btn-white; + @include btn-svg; color: $gl-text-color; @@ -222,13 +232,6 @@ } } - svg { - height: 15px; - width: 15px; - position: relative; - top: 2px; - } - svg, .fa { &:not(:last-child) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 12fc3867c32..b0d6c2d9d22 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -737,6 +737,8 @@ @mixin new-style-dropdown($selector: '') { #{$selector}.dropdown-menu, #{$selector}.dropdown-menu-nav { + margin-bottom: 24px; + li { display: block; padding: 0 1px; @@ -768,7 +770,7 @@ // make sure the text color is not overriden &.text-danger { - // @extend .text-danger; + color: $brand-danger; } &.is-focused, @@ -777,6 +779,11 @@ &:focus { background-color: $dropdown-item-hover-bg; color: $gl-text-color; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } } &.is-active { diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss new file mode 100644 index 00000000000..ebae473df50 --- /dev/null +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -0,0 +1,94 @@ +.feature-highlight { + position: relative; + margin-left: $gl-padding; + width: 20px; + height: 20px; + cursor: pointer; + + &::before { + content: ''; + display: block; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + background-color: $blue-500; + border-radius: 50%; + box-shadow: 0 0 0 rgba($blue-500, 0.4); + animation: pulse-highlight 2s infinite; + } + + &:hover::before, + &.disable-animation::before { + animation: none; + } + + &[disabled]::before { + display: none; + } +} + +.is-showing-fly-out { + .feature-highlight { + display: none; + } +} + +.feature-highlight-popover-content { + display: none; + + hr { + margin: $gl-padding * 0.5 0; + } + + .btn-link { + @include btn-svg; + + svg path { + fill: currentColor; + } + } + + .dismiss-feature-highlight { + padding: 0; + } + + svg:first-child { + width: 100%; + background-color: $indigo-50; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom: 1px solid darken($gray-normal, 8%); + } +} + +.popover .feature-highlight-popover-content { + display: block; +} + +.feature-highlight-popover { + padding: 0; + + .popover-content { + padding: 0; + } +} + +.feature-highlight-popover-sub-content { + padding: 9px 14px; +} + +@include keyframes(pulse-highlight) { + 0% { + box-shadow: 0 0 0 0 rgba($blue-200, 0.4); + } + + 70% { + box-shadow: 0 0 0 10px transparent; + } + + 100% { + box-shadow: 0 0 0 0 transparent; + } +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index a39927eb0df..c5c890b835f 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -267,14 +267,23 @@ // TODO: change global style .ajax-project-dropdown, +body[data-page="projects:edit"] #select2-drop, body[data-page="projects:new"] #select2-drop, +body[data-page="projects:merge_requests:edit"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, body[data-page="projects:blob:edit"] #select2-drop { &.select2-drop { + border: 1px solid $dropdown-border-color; + border-radius: $border-radius-base; color: $gl-text-color; } + &.select2-drop-above { + border-top: none; + margin-top: -4px; + } + .select2-results { .select2-no-results, .select2-searching, diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 0213e7aa9d9..e8ca5cedaee 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -143,8 +143,12 @@ ul.related-merge-requests > li { } } -.issue-form .select2-container { - width: 250px !important; +.issue-form { + @include new-style-dropdown; + + .select2-container { + width: 250px !important; + } } .issues-footer { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 8932cff22a8..5d7c85b16ef 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -23,6 +23,8 @@ .new-note, .note-edit-form { .note-form-actions { + @include new-style-dropdown; + position: relative; margin: $gl-padding 0 0; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 19caefa1961..dd600a27545 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -800,8 +800,10 @@ pre.light-well { } } -.new_protected_branch, +.new-protected-branch, .new-protected-tag { + @include new-style-dropdown; + label { margin-top: 6px; font-weight: $gl-font-weight-normal; @@ -821,19 +823,9 @@ pre.light-well { .protected-branches-list, .protected-tags-list { - margin-bottom: 30px; - - a { - color: $gl-text-color; - - &:hover { - color: $gl-link-color; - } + @include new-style-dropdown; - &.is-active { - font-weight: $gl-font-weight-bold; - } - } + margin-bottom: 30px; .settings-message { margin: 0; diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index a6282187d34..ef9cdd058a9 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -305,14 +305,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def issuable_state_scope(issuable) - if issuable.respond_to?(:merged?) && issuable.merged? - :merged - else - issuable.open? ? :opened : :closed - end - end - def issuable_templates(issuable) @issuable_templates ||= case issuable diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 853ce827061..3d0fdce6a43 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -47,13 +47,6 @@ module IssuesHelper end end - def bulk_update_milestone_options - milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a - milestones.unshift(Milestone::None) - - options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) - end - def milestone_options(object) milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? @@ -93,14 +86,6 @@ module IssuesHelper return 'hidden' if issue.closed? == closed end - def merge_requests_sentence(merge_requests) - # Sorting based on the `!123` or `group/project!123` reference will sort - # local merge requests first. - merge_requests.map do |merge_request| - merge_request.to_reference(@project) - end.sort.to_sentence(last_word_connector: ', or ') - end - def confidential_icon(issue) icon('eye-slash') if issue.confidential? end @@ -148,18 +133,6 @@ module IssuesHelper end.to_h end - def due_date_options - options = [ - Issue::AnyDueDate, - Issue::NoDueDate, - Issue::DueThisWeek, - Issue::DueThisMonth, - Issue::Overdue - ] - - options_from_collection_for_select(options, 'name', 'title', params[:due_date]) - end - def link_to_discussions_to_resolve(merge_request, single_discussion = nil) link_text = merge_request.to_reference link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ca9a350ea79..35d14b6e297 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -305,6 +305,10 @@ module Ci @stage_seeds ||= config_processor.stage_seeds(self) end + def has_kubernetes_active? + project.kubernetes_service&.active? + end + def has_stage_seeds? stage_seeds.any? end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7a817eedec2..724fb4ccef1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -957,13 +957,6 @@ class MergeRequest < ActiveRecord::Base private def write_ref - target_project.repository.with_repo_branch_commit( - source_project.repository, source_branch) do |commit| - if commit - target_project.repository.write_ref(ref_path, commit.sha) - else - raise Rugged::ReferenceError, 'source repository is empty' - end - end + target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path) end end diff --git a/app/models/project.rb b/app/models/project.rb index b9247fb535a..051c4c8e2ec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -68,7 +68,6 @@ class Project < ActiveRecord::Base acts_as_taggable - attr_accessor :new_default_branch attr_accessor :old_path_with_namespace attr_accessor :template_name attr_writer :pipeline_status diff --git a/app/models/repository.rb b/app/models/repository.rb index 5474c8eeb68..05f2f851162 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,7 +20,6 @@ class Repository delegate :ref_name_for_sha, to: :raw_repository - CommitError = Class.new(StandardError) CreateTreeError = Class.new(StandardError) # Methods that cache data from the Git repository. @@ -95,19 +94,6 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end - # - # Git repository can contains some hidden refs like: - # /refs/notes/* - # /refs/git-as-svn/* - # /refs/pulls/* - # This refs by default not visible in project page and not cloned to client side. - # - # This method return true if repository contains some content visible in project page. - # - def has_visible_content? - branch_count > 0 - end - def commit(ref = 'HEAD') return nil unless exists? @@ -184,7 +170,7 @@ class Repository return false unless newrev - GitOperationService.new(user, self).add_branch(branch_name, newrev) + Gitlab::Git::OperationService.new(user, raw_repository).add_branch(branch_name, newrev) after_create_branch find_branch(branch_name) @@ -196,7 +182,7 @@ class Repository return false unless newrev - GitOperationService.new(user, self).add_tag(tag_name, newrev, options) + Gitlab::Git::OperationService.new(user, raw_repository).add_tag(tag_name, newrev, options) find_tag(tag_name) end @@ -205,7 +191,7 @@ class Repository before_remove_branch branch = find_branch(branch_name) - GitOperationService.new(user, self).rm_branch(branch) + Gitlab::Git::OperationService.new(user, raw_repository).rm_branch(branch) after_remove_branch true @@ -215,7 +201,7 @@ class Repository before_remove_tag tag = find_tag(tag_name) - GitOperationService.new(user, self).rm_tag(tag) + Gitlab::Git::OperationService.new(user, raw_repository).rm_tag(tag) after_remove_tag true @@ -784,16 +770,30 @@ class Repository multi_action(**options) end + def with_branch(user, *args) + result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit| + yield start_commit + end + + newrev, should_run_after_create, should_run_after_create_branch = result + + after_create if should_run_after_create + after_create_branch if should_run_after_create_branch + + newrev + end + # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| index = Gitlab::Git::Index.new(raw_repository) @@ -846,7 +846,8 @@ class Repository end def merge(user, source, merge_request, options = {}) - GitOperationService.new(user, self).with_branch( + with_branch( + user, merge_request.target_branch) do |start_commit| our_commit = start_commit.sha their_commit = source @@ -866,17 +867,18 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end - rescue Repository::CommitError # when merge_index.conflicts? + rescue Gitlab::Git::CommitError # when merge_index.conflicts? false end def revert( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| revert_tree_id = check_revert_content(commit, start_commit.sha) unless revert_tree_id @@ -896,10 +898,11 @@ class Repository def cherry_pick( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) unless cherry_pick_tree_id @@ -921,7 +924,7 @@ class Repository end def resolve_conflicts(user, branch_name, params) - GitOperationService.new(user, self).with_branch(branch_name) do + with_branch(user, branch_name) do committer = user_to_committer(user) create_commit(params.merge(author: committer, committer: committer)) @@ -1011,25 +1014,6 @@ class Repository run_git(args).first.lines.map(&:strip) end - def with_repo_branch_commit(start_repository, start_branch_name) - return yield nil if start_repository.empty_repo? - - if start_repository == self - yield commit(start_branch_name) - else - sha = start_repository.commit(start_branch_name).sha - - if branch_commit = commit(sha) - yield branch_commit - else - with_repo_tmp_commit( - start_repository, start_branch_name, sha) do |tmp_commit| - yield tmp_commit - end - end - end - end - def add_remote(name, url) raw_repository.remote_add(name, url) rescue Rugged::ConfigError @@ -1047,14 +1031,12 @@ class Repository gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) - - # Make sure ref was created, and raise Rugged::ReferenceError when not - raise Rugged::ReferenceError, message if status != 0 + def fetch_source_branch(source_repository, source_branch, local_ref) + raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref) + end - target_ref + def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) + raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight) end def create_ref(ref, ref_path) @@ -1135,12 +1117,6 @@ class Repository private - def run_git(args) - circuit_breaker.perform do - Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo) - end - end - def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1236,16 +1212,4 @@ class Repository .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) .map { |c| commit(c) } end - - def with_repo_tmp_commit(start_repository, start_branch_name, sha) - tmp_ref = fetch_ref( - start_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" - ) - - yield commit(sha) - ensure - delete_refs(tmp_ref) if tmp_ref - end end diff --git a/app/models/user.rb b/app/models/user.rb index 68ec93a3ec5..9d48c82e861 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -644,11 +644,6 @@ class User < ActiveRecord::Base @personal_projects_count ||= personal_projects.count end - def projects_limit_percent - return 100 if projects_limit.zero? - (personal_projects.count.to_f / projects_limit) * 100 - end - def recent_push(project_ids = nil) # Get push events not earlier than 2 hours ago events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) @@ -666,10 +661,6 @@ class User < ActiveRecord::Base end end - def projects_sorted_by_activity - authorized_projects.sorted_by_activity - end - def several_namespaces? owned_groups.any? || masters_groups.any? end diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index dbd0b9ef43a..f96f2931508 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -17,7 +17,7 @@ module Commits new_commit = create_commit! success(result: new_commit) - rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex + rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex error(ex.message) end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index a5ae4927412..53f16a236d2 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -11,26 +11,8 @@ class CompareService end def execute(target_project, target_branch, straight: false) - # If compare with other project we need to fetch ref first - target_project.repository.with_repo_branch_commit( - start_project.repository, - start_branch_name) do |commit| - break unless commit + raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight) - compare(commit.sha, target_project, target_branch, straight: straight) - end - end - - private - - def compare(source_sha, target_project, target_branch, straight:) - raw_compare = Gitlab::Git::Compare.new( - target_project.repository.raw_repository, - target_branch, - source_sha, - straight: straight - ) - - Compare.new(raw_compare, target_project, straight: straight) + Compare.new(raw_compare, target_project, straight: straight) if raw_compare end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb deleted file mode 100644 index 6b7a56e6922..00000000000 --- a/app/services/git_operation_service.rb +++ /dev/null @@ -1,159 +0,0 @@ -class GitOperationService - attr_reader :committer, :repository - - def initialize(committer, new_repository) - committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User) - @committer = committer - - @repository = new_repository - end - - def add_branch(branch_name, newrev) - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - oldrev = Gitlab::Git::BLANK_SHA - - update_ref_in_hooks(ref, newrev, oldrev) - end - - def rm_branch(branch) - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name - oldrev = branch.target - newrev = Gitlab::Git::BLANK_SHA - - update_ref_in_hooks(ref, newrev, oldrev) - end - - def add_tag(tag_name, newrev, options = {}) - ref = Gitlab::Git::TAG_REF_PREFIX + tag_name - oldrev = Gitlab::Git::BLANK_SHA - - with_hooks(ref, newrev, oldrev) do |service| - # We want to pass the OID of the tag object to the hooks. For an - # annotated tag we don't know that OID until after the tag object - # (raw_tag) is created in the repository. That is why we have to - # update the value after creating the tag object. Only the - # "post-receive" hook will receive the correct value in this case. - raw_tag = repository.rugged.tags.create(tag_name, newrev, options) - service.newrev = raw_tag.target_id - end - end - - def rm_tag(tag) - ref = Gitlab::Git::TAG_REF_PREFIX + tag.name - oldrev = tag.target - newrev = Gitlab::Git::BLANK_SHA - - update_ref_in_hooks(ref, newrev, oldrev) do - repository.rugged.tags.delete(tag_name) - end - end - - # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, - # it would be created from `start_branch_name`. - # If `start_project` is passed, and the branch doesn't exist, - # it would try to find the commits from it instead of current repository. - def with_branch( - branch_name, - start_branch_name: nil, - start_project: repository.project, - &block) - - start_repository = start_project.repository - start_branch_name = nil if start_repository.empty_repo? - - if start_branch_name && !start_repository.branch_exists?(start_branch_name) - raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}" - end - - update_branch_with_hooks(branch_name) do - repository.with_repo_branch_commit( - start_repository, - start_branch_name || branch_name, - &block) - end - end - - private - - def update_branch_with_hooks(branch_name) - update_autocrlf_option - - was_empty = repository.empty? - - # Make commit - newrev = yield - - unless newrev - raise Repository::CommitError.new('Failed to create commit') - end - - branch = repository.find_branch(branch_name) - oldrev = find_oldrev_from_branch(newrev, branch) - - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - update_ref_in_hooks(ref, newrev, oldrev) - - # If repo was empty expire cache - repository.after_create if was_empty - repository.after_create_branch if - was_empty || Gitlab::Git.blank_ref?(oldrev) - - newrev - end - - def find_oldrev_from_branch(newrev, branch) - return Gitlab::Git::BLANK_SHA unless branch - - oldrev = branch.target - - if oldrev == repository.rugged.merge_base(newrev, branch.target) - oldrev - else - raise Repository::CommitError.new('Branch diverged') - end - end - - def update_ref_in_hooks(ref, newrev, oldrev) - with_hooks(ref, newrev, oldrev) do - update_ref(ref, newrev, oldrev) - end - end - - def with_hooks(ref, newrev, oldrev) - Gitlab::Git::HooksService.new.execute( - committer, - repository, - oldrev, - newrev, - ref) do |service| - - yield(service) - end - end - - # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites. - def update_ref(ref, newrev, oldrev) - # We use 'git update-ref' because libgit2/rugged currently does not - # offer 'compare and swap' ref updates. Without compare-and-swap we can - # (and have!) accidentally reset the ref to an earlier state, clobbering - # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - _, status = Gitlab::Popen.popen( - command, - repository.path_to_repo) do |stdin| - stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") - end - - unless status.zero? - raise Repository::CommitError.new( - "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ - " Please refresh and try again.") - end - end - - def update_autocrlf_option - if repository.raw_repository.autocrlf != :input - repository.raw_repository.autocrlf = :input - end - end -end diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index c91602fcff7..30bf1384b22 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -22,10 +22,10 @@ %b Tag list: = build[:tag_list].to_a.join(", ") %br - %b Refs only: + %b Only policy: = @jobs[build[:name].to_sym][:only].to_a.join(", ") %br - %b Refs except: + %b Except policy: = @jobs[build[:name].to_sym][:except].to_a.join(", ") %br %b Environment: diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg new file mode 100644 index 00000000000..1522c9d51c9 --- /dev/null +++ b/app/views/feature_highlight/_issue_boards.svg @@ -0,0 +1,98 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> + <path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + <path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> + <filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd"> + <path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/> + <g transform="translate(11 23)"> + <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> + <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/> + <g transform="translate(5 10)"> + <use fill="black" filter="url(#a)" xlink:href="#b"/> + <use fill="#F9F9F9" xlink:href="#b"/> + </g> + <g transform="translate(5 42)"> + <use fill="black" filter="url(#c)" xlink:href="#d"/> + <use fill="#FEF0E8" xlink:href="#d"/> + <path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/> + <path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/> + <path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/> + </g> + </g> + <path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/> + <g transform="translate(145 28)"> + <mask id="f" fill="white"> + <use xlink:href="#e"/> + </mask> + <use fill="#FFFFFF" xlink:href="#e"/> + <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/> + <g transform="translate(5 10)"> + <use fill="black" filter="url(#g)" xlink:href="#h"/> + <use fill="#F9F9F9" xlink:href="#h"/> + </g> + <g transform="translate(5 42)"> + <use fill="black" filter="url(#i)" xlink:href="#j"/> + <use fill="#FEF0E8" xlink:href="#j"/> + <path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/> + <path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/> + <path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/> + </g> + </g> + <path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/> + <g transform="translate(78 16)"> + <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> + <g transform="translate(5 10)"> + <use fill="black" filter="url(#k)" xlink:href="#l"/> + <use fill="#EFEDF8" xlink:href="#l"/> + <path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/> + <path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/> + <path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/> + </g> + <g transform="translate(5 42)"> + <use fill="black" filter="url(#m)" xlink:href="#n"/> + <use fill="#F9F9F9" xlink:href="#n"/> + </g> + <g transform="translate(5 74)"> + <rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/> + <use fill="black" filter="url(#o)" xlink:href="#p"/> + <use fill="#F9F9F9" xlink:href="#p"/> + </g> + <path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/> + </g> + </g> +</svg> diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index f5361c7af0c..760c4c97c33 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -99,6 +99,20 @@ = link_to project_boards_path(@project), title: 'Board' do %span Board + .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } } + .feature-highlight-popover-content + = render 'feature_highlight/issue_boards.svg' + .feature-highlight-popover-sub-content + %span= _('Use') + = link_to 'Issue Boards', project_boards_path(@project) + %span= _('to create customized software development workflows like') + %strong= _('Scrum') + %span= _('or') + %strong= _('Kanban') + %hr + %button.btn-link.dismiss-feature-highlight{ type: 'button' } + %span= _("Got it! Don't show this again") + = custom_icon('thumbs_up') = nav_link(controller: :labels) do = link_to project_labels_path(@project), title: 'Labels' do diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg new file mode 100644 index 00000000000..7267462418e --- /dev/null +++ b/app/views/shared/icons/_thumbs_up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg> diff --git a/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml new file mode 100644 index 00000000000..593e74593c4 --- /dev/null +++ b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml @@ -0,0 +1,5 @@ +--- +title: Deprecate custom SSH client configuration for the git user +merge_request: 13930 +author: +type: deprecated diff --git a/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml b/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml new file mode 100644 index 00000000000..00c38a0c671 --- /dev/null +++ b/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml @@ -0,0 +1,5 @@ +--- +title: Add CI/CD active kubernetes job policy +merge_request: 13849 +author: +type: added diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index c9018f3bf0e..d6c3c84851b 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -410,3 +410,9 @@ :why: https://gitlab.com/gitlab-com/organization/issues/116 :versions: [] :when: 2017-09-01 17:17:51.996511844 Z +- - :blacklist + - Facebook BSD+PATENTS + - :who: Nick Thomas <nick@gitlab.com> + :why: https://gitlab.com/gitlab-com/organization/issues/117 + :versions: [] + :when: 2017-09-04 12:59:51.150798717 Z diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 545c01e1156..c5704ac5857 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -508,7 +508,7 @@ production: &base failure_count_threshold: 10 # number of failures before stopping attempts failure_wait_time: 30 # Seconds after an access failure before allowing access again failure_reset_time: 1800 # Time in seconds to expire failures - storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt + storage_timeout: 30 # Time in seconds to wait before aborting a storage access attempt ## Backup settings diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index abf4ec7dbf8..cacfd2ed254 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -427,16 +427,16 @@ a "key: value" pair. Be careful when using special characters: are executed in `parallel`. For more info about the use of `stage` please check [stages](#stages). -### only and except +### only and except (simplified) -`only` and `except` are two parameters that set a refs policy to limit when -jobs are built: +`only` and `except` are two parameters that set a job policy to limit when +jobs are created: 1. `only` defines the names of branches and tags for which the job will run. 2. `except` defines the names of branches and tags for which the job will **not** run. -There are a few rules that apply to the usage of refs policy: +There are a few rules that apply to the usage of job policy: * `only` and `except` are inclusive. If both `only` and `except` are defined in a job specification, the ref is filtered by `only` and `except`. @@ -497,6 +497,36 @@ job: The above example will run `job` for all branches on `gitlab-org/gitlab-ce`, except master. +### only and except (complex) + +> Introduced in GitLab 10.0 + +> This an _alpha_ feature, and it it subject to change at any time without + prior notice! + +Since GitLab 10.0 it is possible to define a more elaborate only/except job +policy configuration. + +GitLab now supports both, simple and complex strategies, so it is possible to +use an array and a hash configuration scheme. + +Two keys are now available: `refs` and `kubernetes`. Refs strategy equals to +simplified only/except configuration, whereas kubernetes strategy accepts only +`active` keyword. + +See the example below. Job is going to be created only when pipeline has been +scheduled or runs for a `master` branch, and only if kubernetes service is +active in the project. + +```yaml +job: + only: + refs: + - master + - schedules + kubernetes: active +``` + ### Job variables It is possible to define job variables using a `variables` keyword on a job diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 0742b202807..2607353782a 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -28,8 +28,9 @@ As always, the Frontend Architectural Experts are available to help with any Vue All new features built with Vue.js must follow a [Flux architecture][flux]. The main goal we are trying to achieve is to have only one data flow and only one data entry. -In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -, -a Service - that we use to communicate with the server - and a main Vue component. +In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below: + +Each Vue bundle needs a Store - where we keep all the data -,a Service - that we use to communicate with the server - and a main Vue component. Think of the Main Vue Component as the entry point of your application. This is the only smart component that should exist in each Vue feature. @@ -74,6 +75,59 @@ provided as a prop to the main component. Don't forget to follow [these steps.][page_specific_javascript] +### Bootstrapping Gotchas +#### Providing data from Haml to JavaScript +While mounting a Vue application may be a need to provide data from Rails to JavaScript. +To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application. + +_Note:_ You should only do this while initing the application, because the mounted element will be replaced with Vue-generated DOM. + +The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function +instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to +create a fixture or an HTML element in the unit test. See the following example: + +```javascript +// haml +.js-vue-app{ data: { endpoint: 'foo' }} + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '.js-vue-app', + data() { + const dataset = this.$options.el.dataset; + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('my-component', { + props: { + endpoint: this.isLoading, + }, + }); + }, +})); +``` + +#### Accessing the `gl` object +When we need to query the `gl` object for data that won't change during the application's lyfecyle, we should do it in the same place where we query the DOM. +By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier. +It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component: + +##### example: +```javascript + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '.js-vue-app', + render(createElement) { + return createElement('my-component', { + props: { + username: gon.current_username, + }, + }); + }, +})); +``` + ### A folder for Components This folder holds all components that are specific of this new feature. @@ -89,6 +143,29 @@ in one table would not be a good use of this pattern. You can read more about components in Vue.js site, [Component System][component-system] +#### Components Gotchas +1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component. +A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them. +The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies. + +```javascript +// bad +import svg from 'svg.svg'; +data() { + return { + myIcon: svg, + }; +}; + +// good +import svg from 'svg.svg'; +computed: { + myIcon() { + return svg; + } +} +``` + ### A folder for the Store The Store is a class that allows us to manage the state in a single @@ -430,11 +507,23 @@ describe('Todos App', () => { }); }); ``` +#### `mountComponent` helper +There is an helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props: + +```javascript +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper.js' +import component from 'component.vue' + +const Component = Vue.extend(component); +const data = {prop: 'foo'}; +const vm = mountComponent(Component, data); +``` + #### Test the component's output The main return value of a Vue component is the rendered output. In order to test the component we need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that: - ### Stubbing API responses [Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with the response we need: @@ -481,6 +570,198 @@ new Component({ new Component().$mount(); ``` +## Vuex +To manage the state of an application you may use [Vuex][vuex-docs]. + +_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs]. + +### Separation of concerns +Vuex is composed of State, Getters, Mutations, Actions and Modules. + +When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state. +_Note:_ The action itself will not update the state, only a mutation should update the state. + +#### File structure +When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well: + +``` +└── store + ├── index.js # where we assemble modules and export the store + ├── actions.js # actions + ├── mutations.js # mutations + ├── getters.js # getters + └── mutation_types.js # mutation types +``` +The following examples show an application that lists and adds users to the state. + +##### `index.js` +This is the entry point for our store. You can use the following as a guide: + +```javascript +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + actions, + getters, + state: { + users: [], + }, +}); +``` +_Note:_ If the state of the application is too complex, an individual file for the state may be better. + +#### `actions.js` +An action commits a mutatation. In this file, we will write the actions that will call the respective mutation: + +```javascript + import * as types from './mutation-types' + + export const addUser = ({ commit }, user) => { + commit(types.ADD_USER, user); + }; +``` + +To dispatch an action from a component, use the `mapActions` helper: +```javascript +import { mapActions } from 'vuex'; + +{ + methods: { + ...mapActions([ + 'addUser', + ]), + onClickUser(user) { + this.addUser(user); + }, + }, +}; +``` + +#### `getters.js` +Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`: + +```javascript +// get all the users with pets +export getUsersWithPets = (state, getters) => { + return state.users.filter(user => user.pet !== undefined); +}; +``` + +To access a getter from a component, use the `mapGetters` helper: +```javascript +import { mapGetters } from 'vuex'; + +{ + computed: { + ...mapGetters([ + 'getUsersWithPets', + ]), + }, +}; +``` + +#### `mutations.js` +The only way to actually change state in a Vuex store is by committing a mutation. + +```javascript + import * as types from './mutation-types' + export default { + [types.ADD_USER](state, user) { + state.users.push(user); + }, + }; +``` + +#### `mutations_types.js` +From [vuex mutations docs][vuex-mutations]: +> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application. + +```javascript +export const ADD_USER = 'ADD_USER'; +``` + +### How to include the store in your application +The store should be included in the main component of your application: +```javascript + // app.vue + import store from 'store'; // it will include the index.js file + + export default { + name: 'application', + store, + ... + }; +``` + +### Vuex Gotchas +1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs: + + > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. + + ```javascript + // component.vue + + // bad + created() { + this.$store.commit('mutation'); + } + + // good + created() { + this.$store.dispatch('action'); + } + ``` +1. When possible, use mutation types instead of hardcoding strings. It will be less error prone. +1. The State will be accessible in all components descending from the use where the store is instantiated. + +### Testing Vuex +#### Testing Vuex concerns +Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations. + +#### Testing components that need a store +Smaller components might use `store` properties to access the data. +In order to write unit tests for those components, we need to include the store and provide the correct state: + +```javascript +//component_spec.js +import Vue from 'vue'; +import store from './store'; +import component from './component.vue' + +describe('component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueActions); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should show a user', () => { + const user = { + name: 'Foo', + age: '30', + }; + + // populate the store + store.dipatch('addUser', user); + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); +}); +``` + [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards [environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments @@ -493,3 +774,7 @@ new Component().$mount(); [vue-test]: https://vuejs.org/v2/guide/unit-testing.html [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [flux]: https://facebook.github.io/flux +[vuex-docs]: https://vuex.vuejs.org +[vuex-structure]: https://vuex.vuejs.org/en/structure.html +[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html +[vuex-testing]: https://vuex.vuejs.org/en/testing.html diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 60da7b9166d..9a5811d8474 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -64,6 +64,7 @@ Libraries with the following licenses are unacceptable for use: - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. - [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU]. +- [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation. ## Requesting Approval for Licenses @@ -103,5 +104,7 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL [Org-Repo]: https://gitlab.com/gitlab-com/organization [UNLICENSE]: https://unlicense.org +[Facebook]: https://code.facebook.com/pages/850928938376556 +[x-list]: https://www.apache.org/legal/resolved.html#category-x [Acceptable-Licenses]: #acceptable-licenses [Unacceptable-Licenses]: #unacceptable-licenses diff --git a/doc/ssh/README.md b/doc/ssh/README.md index cf28f1a2eca..793de9d777c 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -193,6 +193,38 @@ How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Ecl [winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen +## SSH on the GitLab server + +GitLab integrates with the system-installed SSH daemon, designating a user +(typically named `git`) through which all access requests are handled. Users +connecting to the GitLab server over SSH are identified by their SSH key instead +of their username. + +SSH *client* operations performed on the GitLab server wil be executed as this +user. Although it is possible to modify the SSH configuration for this user to, +e.g., provide a private SSH key to authenticate these requests by, this practice +is **not supported** and is strongly discouraged as it presents significant +security risks. + +The GitLab check process includes a check for this condition, and will direct you +to this section if your server is configured like this, e.g.: + +``` +$ gitlab-rake gitlab:check +# ... +Git user has default SSH configuration? ... no + Try fixing it: + mkdir ~/gitlab-check-backup-1504540051 + sudo mv /var/lib/git/.ssh/id_rsa ~/gitlab-check-backup-1504540051 + sudo mv /var/lib/git/.ssh/id_rsa.pub ~/gitlab-check-backup-1504540051 + For more information see: + doc/ssh/README.md in section "SSH on the GitLab server" + Please fix the error above and rerun the checks. +``` + +Remove the custom configuration as soon as you're able to. These customizations +are *explicitly not supported* and may stop working at any time. + ## Troubleshooting If on Git clone you are prompted for a password like `git@gitlab.com's password:` diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 3a4911b23b0..62b44389b15 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -20,24 +20,6 @@ module Ci raise ValidationError, e.message end - def jobs_for_ref(ref, tag = false, source = nil) - @jobs.select do |_, job| - process?(job[:only], job[:except], ref, tag, source) - end - end - - def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_ref(ref, tag, source).select do |_, job| - job[:stage] == stage - end - end - - def builds_for_ref(ref, tag = false, source = nil) - jobs_for_ref(ref, tag, source).map do |name, _| - build_attributes(name) - end - end - def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| build_attributes(name) @@ -52,8 +34,7 @@ module Ci def stage_seeds(pipeline) seeds = @stages.uniq.map do |stage| - builds = builds_for_stage_and_ref( - stage, pipeline.ref, pipeline.tag?, pipeline.source) + builds = pipeline_stage_builds(stage, pipeline) Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? end @@ -101,6 +82,34 @@ module Ci private + def pipeline_stage_builds(stage, pipeline) + builds = builds_for_stage_and_ref( + stage, pipeline.ref, pipeline.tag?, pipeline.source) + + builds.select do |build| + job = @jobs[build.fetch(:name).to_sym] + has_kubernetes = pipeline.has_kubernetes_active? + only_kubernetes = job.dig(:only, :kubernetes) + except_kubernetes = job.dig(:except, :kubernetes) + + [!only_kubernetes && !except_kubernetes, + only_kubernetes && has_kubernetes, + except_kubernetes && !has_kubernetes].any? + end + end + + def jobs_for_ref(ref, tag = false, source = nil) + @jobs.select do |_, job| + process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) + end + end + + def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_ref(ref, tag, source).select do |_, job| + job[:stage] == stage + end + end + def initial_parsing ## # Global config diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 3cdae1cee4f..0027e9ec8c5 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -7,6 +7,7 @@ module Gitlab # class Policy < Simplifiable strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) } + strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) } class RefsPolicy < Entry::Node include Entry::Validatable @@ -14,6 +15,27 @@ module Gitlab validations do validates :config, array_of_strings_or_regexps: true end + + def value + { refs: @config } + end + end + + class ComplexPolicy < Entry::Node + include Entry::Validatable + include Entry::Attributable + + attributes :refs, :kubernetes + + validations do + validates :config, presence: true + validates :config, allowed_keys: %i[refs kubernetes] + + with_options allow_nil: true do + validates :refs, array_of_strings_or_regexps: true + validates :kubernetes, allowed_values: %w[active] + end + end end class UnknownStrategy < Entry::Node diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index b2ca3c881e4..0159179f0a9 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -14,6 +14,14 @@ module Gitlab end end + class AllowedValuesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless options[:in].include?(value.to_s) + record.errors.add(attribute, "unknown value: #{value}") + end + end + end + class ArrayOfStringsValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index d671867e7c7..90f83e0f810 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -18,7 +18,7 @@ module Gitlab new(merge_request, project).tap do |file_collection| project .repository - .with_repo_branch_commit(merge_request.target_project.repository, merge_request.target_branch) do + .with_repo_branch_commit(merge_request.target_project.repository.raw_repository, merge_request.target_branch) do yield file_collection end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index b6449f27034..8c9acbc9fbe 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -5,6 +5,7 @@ module Gitlab BRANCH_REF_PREFIX = "refs/heads/".freeze CommandError = Class.new(StandardError) + CommitError = Class.new(StandardError) class << self include Gitlab::EncodingHelper diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb new file mode 100644 index 00000000000..9e6fca8c80c --- /dev/null +++ b/lib/gitlab/git/operation_service.rb @@ -0,0 +1,168 @@ +module Gitlab + module Git + class OperationService + attr_reader :committer, :repository + + def initialize(committer, new_repository) + committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User) + @committer = committer + + # Refactoring aid + unless new_repository.is_a?(Gitlab::Git::Repository) + raise "expected a Gitlab::Git::Repository, got #{new_repository}" + end + + @repository = new_repository + end + + def add_branch(branch_name, newrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) + end + + def rm_branch(branch) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name + oldrev = branch.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) + end + + def add_tag(tag_name, newrev, options = {}) + ref = Gitlab::Git::TAG_REF_PREFIX + tag_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks(ref, newrev, oldrev) do |service| + # We want to pass the OID of the tag object to the hooks. For an + # annotated tag we don't know that OID until after the tag object + # (raw_tag) is created in the repository. That is why we have to + # update the value after creating the tag object. Only the + # "post-receive" hook will receive the correct value in this case. + raw_tag = repository.rugged.tags.create(tag_name, newrev, options) + service.newrev = raw_tag.target_id + end + end + + def rm_tag(tag) + ref = Gitlab::Git::TAG_REF_PREFIX + tag.name + oldrev = tag.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) do + repository.rugged.tags.delete(tag_name) + end + end + + # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, + # it would be created from `start_branch_name`. + # If `start_project` is passed, and the branch doesn't exist, + # it would try to find the commits from it instead of current repository. + def with_branch( + branch_name, + start_branch_name: nil, + start_repository: repository, + &block) + + # Refactoring aid + unless start_repository.is_a?(Gitlab::Git::Repository) + raise "expected a Gitlab::Git::Repository, got #{start_repository}" + end + + start_branch_name = nil if start_repository.empty_repo? + + if start_branch_name && !start_repository.branch_exists?(start_branch_name) + raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}" + end + + update_branch_with_hooks(branch_name) do + repository.with_repo_branch_commit( + start_repository, + start_branch_name || branch_name, + &block) + end + end + + private + + # Returns [newrev, should_run_after_create, should_run_after_create_branch] + def update_branch_with_hooks(branch_name) + update_autocrlf_option + + was_empty = repository.empty? + + # Make commit + newrev = yield + + unless newrev + raise Gitlab::Git::CommitError.new('Failed to create commit') + end + + branch = repository.find_branch(branch_name) + oldrev = find_oldrev_from_branch(newrev, branch) + + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + update_ref_in_hooks(ref, newrev, oldrev) + + [newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)] + end + + def find_oldrev_from_branch(newrev, branch) + return Gitlab::Git::BLANK_SHA unless branch + + oldrev = branch.target + + if oldrev == repository.rugged.merge_base(newrev, branch.target) + oldrev + else + raise Gitlab::Git::CommitError.new('Branch diverged') + end + end + + def update_ref_in_hooks(ref, newrev, oldrev) + with_hooks(ref, newrev, oldrev) do + update_ref(ref, newrev, oldrev) + end + end + + def with_hooks(ref, newrev, oldrev) + Gitlab::Git::HooksService.new.execute( + committer, + repository, + oldrev, + newrev, + ref) do |service| + + yield(service) + end + end + + # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites. + def update_ref(ref, newrev, oldrev) + # We use 'git update-ref' because libgit2/rugged currently does not + # offer 'compare and swap' ref updates. Without compare-and-swap we can + # (and have!) accidentally reset the ref to an earlier state, clobbering + # commits. See also https://github.com/libgit2/libgit2/issues/1534. + command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] + _, status = Gitlab::Popen.popen( + command, + repository.path) do |stdin| + stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") + end + + unless status.zero? + raise Gitlab::Git::CommitError.new( + "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ + " Please refresh and try again.") + end + end + + def update_autocrlf_option + if repository.autocrlf != :input + repository.autocrlf = :input + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 8709f82bcc4..c67f7724307 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -73,6 +73,10 @@ module Gitlab delegate :exists?, to: :gitaly_repository_client + def ==(other) + path == other.path + end + # Default branch in the repository def root_ref @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled| @@ -740,6 +744,106 @@ module Gitlab end end + def with_repo_branch_commit(start_repository, start_branch_name) + raise "expected Gitlab::Git::Repository, got #{start_repository}" unless start_repository.is_a?(Gitlab::Git::Repository) + + return yield nil if start_repository.empty_repo? + + if start_repository == self + yield commit(start_branch_name) + else + sha = start_repository.commit(start_branch_name).sha + + if branch_commit = commit(sha) + yield branch_commit + else + with_repo_tmp_commit( + start_repository, start_branch_name, sha) do |tmp_commit| + yield tmp_commit + end + end + end + end + + def with_repo_tmp_commit(start_repository, start_branch_name, sha) + tmp_ref = fetch_ref( + start_repository.path, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", + "refs/tmp/#{SecureRandom.hex}/head" + ) + + yield commit(sha) + ensure + delete_refs(tmp_ref) if tmp_ref + end + + def fetch_source_branch(source_repository, source_branch, local_ref) + with_repo_branch_commit(source_repository, source_branch) do |commit| + if commit + write_ref(local_ref, commit.sha) + else + raise Rugged::ReferenceError, 'source repository is empty' + end + end + end + + def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) + with_repo_branch_commit(source_repository, source_branch_name) do |commit| + break unless commit + + Gitlab::Git::Compare.new( + self, + target_branch_name, + commit.sha, + straight: straight + ) + end + end + + def write_ref(ref_path, sha) + rugged.references.create(ref_path, sha, force: true) + end + + def fetch_ref(source_path, source_ref, target_ref) + args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) + message, status = run_git(args) + + # Make sure ref was created, and raise Rugged::ReferenceError when not + raise Rugged::ReferenceError, message if status != 0 + + target_ref + end + + # Refactoring aid; allows us to copy code from app/models/repository.rb + def run_git(args) + circuit_breaker.perform do + popen([Gitlab.config.git.bin_path, *args], path) + end + end + + # Refactoring aid; allows us to copy code from app/models/repository.rb + def commit(ref = 'HEAD') + Gitlab::Git::Commit.find(self, ref) + end + + # Refactoring aid; allows us to copy code from app/models/repository.rb + def empty_repo? + !exists? || !has_visible_content? + end + + # + # Git repository can contains some hidden refs like: + # /refs/notes/* + # /refs/git-as-svn/* + # /refs/pulls/* + # This refs by default not visible in project page and not cloned to client side. + # + # This method return true if repository contains some content visible in project page. + # + def has_visible_content? + branch_count > 0 + end + def gitaly_repository Gitlab::GitalyClient::Util.repository(@storage, @relative_path) end diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb index 2e02787a4f4..7d3ff8c7f58 100644 --- a/lib/gitlab/i18n/po_linter.rb +++ b/lib/gitlab/i18n/po_linter.rb @@ -1,5 +1,3 @@ -require 'simple_po_parser' - module Gitlab module I18n class PoLinter diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb new file mode 100644 index 00000000000..7b486d78cf0 --- /dev/null +++ b/lib/system_check/app/git_user_default_ssh_config_check.rb @@ -0,0 +1,69 @@ +module SystemCheck + module App + class GitUserDefaultSSHConfigCheck < SystemCheck::BaseCheck + # These files are allowed in the .ssh directory. The `config` file is not + # whitelisted as it may change the SSH client's behaviour dramatically. + WHITELIST = %w[ + authorized_keys + authorized_keys2 + known_hosts + ].freeze + + set_name 'Git user has default SSH configuration?' + set_skip_reason 'skipped (git user is not present or configured)' + + def skip? + !home_dir || !File.directory?(home_dir) + end + + def check? + forbidden_files.empty? + end + + def show_error + backup_dir = "~/gitlab-check-backup-#{Time.now.to_i}" + + instructions = forbidden_files.map do |filename| + "sudo mv #{Shellwords.escape(filename)} #{backup_dir}" + end + + try_fixing_it("mkdir #{backup_dir}", *instructions) + for_more_information('doc/ssh/README.md in section "SSH on the GitLab server"') + fix_and_rerun + end + + private + + def git_user + Gitlab.config.gitlab.user + end + + def home_dir + return @home_dir if defined?(@home_dir) + + @home_dir = + begin + File.expand_path("~#{git_user}") + rescue ArgumentError + nil + end + end + + def ssh_dir + return nil unless home_dir + + File.join(home_dir, '.ssh') + end + + def forbidden_files + @forbidden_files ||= + begin + present = Dir[File.join(ssh_dir, '*')] + whitelisted = WHITELIST.map { |basename| File.join(ssh_dir, basename) } + + present - whitelisted + end + end + end + end +end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index e1491f29b5e..f7f2fa2f14c 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -1,4 +1,5 @@ require "gettext_i18n_rails/tasks" +require 'simple_po_parser' namespace :gettext do # Customize list of translatable files diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 1bd36bbe20a..92a3f503fcb 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -33,6 +33,7 @@ namespace :gitlab do SystemCheck::App::RedisVersionCheck, SystemCheck::App::RubyVersionCheck, SystemCheck::App::GitVersionCheck, + SystemCheck::App::GitUserDefaultSSHConfigCheck, SystemCheck::App::ActiveUsersCheck ] diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 2eb6fab129d..ad2db1a34f4 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -18,23 +18,25 @@ feature 'Import/Export - project import integration test', js: true do context 'when selecting the namespace' do let(:user) { create(:admin) } - let!(:namespace) { create(:namespace, name: "asd", owner: user) } + let!(:namespace) { create(:namespace, name: 'asd', owner: user) } + let(:project_path) { 'test-project-path' + SecureRandom.hex } context 'prefilled the path' do scenario 'user imports an exported project successfully' do visit new_project_path select2(namespace.id, from: '#project_namespace_id') - fill_in :project_path, with: 'test-project-path', visible: true + fill_in :project_path, with: project_path, visible: true click_link 'GitLab export' expect(page).to have_content('Import an exported GitLab project') - expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original + expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") + expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\h*\z/).and_call_original attach_file('file', file) + click_on 'Import project' - expect { click_on 'Import project' }.to change { Project.count }.by(1) + expect(Project.count).to eq(1) project = Project.last expect(project).not_to be_nil @@ -64,7 +66,7 @@ feature 'Import/Export - project import integration test', js: true do end scenario 'invalid project' do - namespace = create(:namespace, name: "asd", owner: user) + namespace = create(:namespace, name: 'asdf', owner: user) project = create(:project, namespace: namespace) visit new_project_path diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index dc3100311f8..ddf881a7b6f 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -58,16 +58,6 @@ describe IssuesHelper do end end - describe "merge_requests_sentence" do - subject { merge_requests_sentence(merge_requests)} - let(:merge_requests) do - [build(:merge_request, iid: 1), build(:merge_request, iid: 2), - build(:merge_request, iid: 3)] - end - - it { is_expected.to eq("!1, !2, or !3") } - end - describe '#award_user_list' do it "returns a comma-separated list of the first X users" do user = build_stubbed(:user, name: 'Joe') diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js new file mode 100644 index 00000000000..114d282e48a --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js @@ -0,0 +1,219 @@ +import Cookies from 'js-cookie'; +import { + getCookieName, + getSelector, + showPopover, + hidePopover, + dismiss, + mouseleave, + mouseenter, + setupDismissButton, +} from '~/feature_highlight/feature_highlight_helper'; + +describe('feature highlight helper', () => { + describe('getCookieName', () => { + it('returns `feature-highlighted-` prefix', () => { + const cookieId = 'cookieId'; + expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`); + }); + }); + + describe('getSelector', () => { + it('returns js-feature-highlight selector', () => { + const highlightId = 'highlightId'; + expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`); + }); + }); + + describe('showPopover', () => { + it('returns true when popover is shown', () => { + const context = { + hasClass: () => false, + popover: () => {}, + addClass: () => {}, + }; + + expect(showPopover.call(context)).toEqual(true); + }); + + it('returns false when popover is already shown', () => { + const context = { + hasClass: () => true, + }; + + expect(showPopover.call(context)).toEqual(false); + }); + + it('shows popover', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + addClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('show'); + done(); + }); + + showPopover.call(context); + }); + + it('adds disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + addClass: () => {}, + }; + + spyOn(context, 'addClass').and.callFake((classNames) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + done(); + }); + + showPopover.call(context); + }); + }); + + describe('hidePopover', () => { + it('returns true when popover is hidden', () => { + const context = { + hasClass: () => true, + popover: () => {}, + removeClass: () => {}, + }; + + expect(hidePopover.call(context)).toEqual(true); + }); + + it('returns false when popover is already hidden', () => { + const context = { + hasClass: () => false, + }; + + expect(hidePopover.call(context)).toEqual(false); + }); + + it('hides popover', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + removeClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('hide'); + done(); + }); + + hidePopover.call(context); + }); + + it('removes disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + removeClass: () => {}, + }; + + spyOn(context, 'removeClass').and.callFake((classNames) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + done(); + }); + + hidePopover.call(context); + }); + }); + + describe('dismiss', () => { + const context = { + hide: () => {}, + }; + + beforeEach(() => { + spyOn(Cookies, 'set').and.callFake(() => {}); + spyOn(hidePopover, 'call').and.callFake(() => {}); + spyOn(context, 'hide').and.callFake(() => {}); + dismiss.call(context); + }); + + it('sets cookie to true', () => { + expect(Cookies.set).toHaveBeenCalled(); + }); + + it('calls hide popover', () => { + expect(hidePopover.call).toHaveBeenCalled(); + }); + + it('calls hide', () => { + expect(context.hide).toHaveBeenCalled(); + }); + }); + + describe('mouseleave', () => { + it('calls hide popover if .popover:hover is false', () => { + const fakeJquery = { + length: 0, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(hidePopover, 'call'); + mouseleave(); + expect(hidePopover.call).toHaveBeenCalled(); + }); + + it('does not call hide popover if .popover:hover is true', () => { + const fakeJquery = { + length: 1, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(hidePopover, 'call'); + mouseleave(); + expect(hidePopover.call).not.toHaveBeenCalled(); + }); + }); + + describe('mouseenter', () => { + const context = {}; + + it('shows popover', () => { + spyOn(showPopover, 'call').and.returnValue(false); + mouseenter.call(context); + expect(showPopover.call).toHaveBeenCalled(); + }); + + it('registers mouseleave event if popover is showed', (done) => { + spyOn(showPopover, 'call').and.returnValue(true); + spyOn($.fn, 'on').and.callFake((eventName) => { + expect(eventName).toEqual('mouseleave'); + done(); + }); + mouseenter.call(context); + }); + + it('does not register mouseleave event if popover is not showed', () => { + spyOn(showPopover, 'call').and.returnValue(false); + const spy = spyOn($.fn, 'on').and.callFake(() => {}); + mouseenter.call(context); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('setupDismissButton', () => { + it('registers click event callback', (done) => { + const context = { + getAttribute: () => 'popoverId', + dataset: { + highlight: 'cookieId', + }, + }; + + spyOn($.fn, 'on').and.callFake((event) => { + expect(event).toEqual('click'); + done(); + }); + setupDismissButton.call(context); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js new file mode 100644 index 00000000000..7feb361edec --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js @@ -0,0 +1,45 @@ +import domContentLoaded from '~/feature_highlight/feature_highlight_options'; +import bp from '~/breakpoints'; + +describe('feature highlight options', () => { + describe('domContentLoaded', () => { + const highlightOrder = []; + + beforeEach(() => { + // Check for when highlightFeatures is called + spyOn(highlightOrder, 'find').and.callFake(() => {}); + }); + + it('should not call highlightFeatures when breakpoint is xs', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).not.toHaveBeenCalled(); + }); + + it('should not call highlightFeatures when breakpoint is sm', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).not.toHaveBeenCalled(); + }); + + it('should not call highlightFeatures when breakpoint is md', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).not.toHaveBeenCalled(); + }); + + it('should call highlightFeatures when breakpoint is lg', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js new file mode 100644 index 00000000000..6abe8425ee7 --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -0,0 +1,122 @@ +import Cookies from 'js-cookie'; +import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; +import * as featureHighlight from '~/feature_highlight/feature_highlight'; + +describe('feature highlight', () => { + describe('setupFeatureHighlightPopover', () => { + const selector = '.js-feature-highlight[data-highlight=test]'; + beforeEach(() => { + setFixtures(` + <div> + <div class="js-feature-highlight" data-highlight="test" disabled> + Trigger + </div> + </div> + <div class="feature-highlight-popover-content"> + Content + <div class="dismiss-feature-highlight"> + Dismiss + </div> + </div> + `); + spyOn(window, 'addEventListener'); + spyOn(window, 'removeEventListener'); + featureHighlight.setupFeatureHighlightPopover('test', 0); + }); + + it('setups popover content', () => { + const $popoverContent = $('.feature-highlight-popover-content'); + const outerHTML = $popoverContent.prop('outerHTML'); + + expect($(selector).data('content')).toEqual(outerHTML); + }); + + it('setups mouseenter', () => { + const showSpy = spyOn(featureHighlightHelper.showPopover, 'call'); + $(selector).trigger('mouseenter'); + + expect(showSpy).toHaveBeenCalled(); + }); + + it('setups debounced mouseleave', (done) => { + const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call'); + $(selector).trigger('mouseleave'); + + // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce + setTimeout(() => { + expect(hideSpy).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('setups inserted.bs.popover', () => { + $(selector).trigger('mouseenter'); + const popoverId = $(selector).attr('aria-describedby'); + const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); + + $(`#${popoverId} .dismiss-feature-highlight`).click(); + expect(spyEvent).toHaveBeenTriggered(); + }); + + it('setups show.bs.popover', () => { + $(selector).trigger('show.bs.popover'); + expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + }); + + it('setups hide.bs.popover', () => { + $(selector).trigger('hide.bs.popover'); + expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + }); + + it('removes disabled attribute', () => { + expect($('.js-feature-highlight').is(':disabled')).toEqual(false); + }); + + it('displays popover', () => { + expect($(selector).attr('aria-describedby')).toBeFalsy(); + $(selector).trigger('mouseenter'); + expect($(selector).attr('aria-describedby')).toBeTruthy(); + }); + }); + + describe('shouldHighlightFeature', () => { + it('should return false if element is not found', () => { + spyOn(document, 'querySelector').and.returnValue(null); + spyOn(Cookies, 'get').and.returnValue(null); + + expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); + }); + + it('should return false if previouslyDismissed', () => { + spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); + spyOn(Cookies, 'get').and.returnValue('true'); + + expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); + }); + + it('should return true if element is found and not previouslyDismissed', () => { + spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); + spyOn(Cookies, 'get').and.returnValue(null); + + expect(featureHighlight.shouldHighlightFeature()).toBeTruthy(); + }); + }); + + describe('highlightFeatures', () => { + it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => { + // Mimic shouldHighlightFeature set to true + const highlightOrder = ['issue-boards']; + spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]); + + expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true); + }); + + it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => { + // Mimic shouldHighlightFeature set to false + const highlightOrder = ['issue-boards']; + spyOn(highlightOrder, 'find').and.returnValue(null); + + expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false); + }); + }); +}); diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c70a4cb55fe..1efd3113a43 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -164,9 +164,46 @@ module Ci expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' end end + + context 'when kubernetes policy is specified' do + let(:pipeline) { create(:ci_empty_pipeline) } + + let(:config) do + YAML.dump( + spinach: { stage: 'test', script: 'spinach' }, + production: { + stage: 'deploy', + script: 'cap', + only: { kubernetes: 'active' } + } + ) + end + + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + it 'returns seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when kubernetes is not active' do + it 'does not return seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end end - describe "#builds_for_ref" do + describe "#builds_for_stage_and_ref" do let(:type) { 'test' } it "returns builds if no branch specified" do diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 36a84da4a52..5e83abf645b 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -16,8 +16,8 @@ describe Gitlab::Ci::Config::Entry::Policy do end describe '#value' do - it 'returns key value' do - expect(entry.value).to eq config + it 'returns refs hash' do + expect(entry.value).to eq(refs: config) end end end @@ -56,6 +56,50 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + context 'when using complex policy' do + context 'when specifiying refs policy' do + let(:config) { { refs: ['master'] } } + + it 'is a correct configuraton' do + expect(entry).to be_valid + expect(entry.value).to eq(refs: %w[master]) + end + end + + context 'when specifying kubernetes policy' do + let(:config) { { kubernetes: 'active' } } + + it 'is a correct configuraton' do + expect(entry).to be_valid + expect(entry.value).to eq(kubernetes: 'active') + end + end + + context 'when specifying invalid kubernetes policy' do + let(:config) { { kubernetes: 'something' } } + + it 'reports an error about invalid policy' do + expect(entry.errors).to include /unknown value: something/ + end + end + + context 'when specifying unknown policy' do + let(:config) { { refs: ['master'], invalid: :something } } + + it 'returns error about invalid key' do + expect(entry.errors).to include /unknown keys: invalid/ + end + end + + context 'when policy is empty' do + let(:config) { {} } + + it 'is not a valid configuration' do + expect(entry.errors).to include /can't be blank/ + end + end + end + context 'when policy strategy does not match' do let(:config) { 'string strategy' } diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index cd5c2b99751..3a962ba7f22 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'simple_po_parser' describe Gitlab::I18n::PoLinter do let(:linter) { described_class.new(po_path) } diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb new file mode 100644 index 00000000000..7125bfcab59 --- /dev/null +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe SystemCheck::App::GitUserDefaultSSHConfigCheck do + let(:username) { '_this_user_will_not_exist_unless_it_is_stubbed' } + let(:base_dir) { Dir.mktmpdir } + let(:home_dir) { File.join(base_dir, "/var/lib/#{username}") } + let(:ssh_dir) { File.join(home_dir, '.ssh') } + let(:forbidden_file) { 'id_rsa' } + + before do + allow(Gitlab.config.gitlab).to receive(:user).and_return(username) + end + + after do + FileUtils.rm_rf(base_dir) + end + + it 'only whitelists safe files' do + expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts') + end + + describe '#skip?' do + subject { described_class.new.skip? } + + where(user_exists: [true, false], home_dir_exists: [true, false]) + + with_them do + let(:expected_result) { !user_exists || !home_dir_exists } + + before do + stub_user if user_exists + stub_home_dir if home_dir_exists + end + + it { is_expected.to eq(expected_result) } + end + end + + describe '#check?' do + subject { described_class.new.check? } + + before do + stub_user + end + + it 'fails if a forbidden file exists' do + stub_ssh_file(forbidden_file) + + is_expected.to be_falsy + end + + it "succeeds if the SSH directory doesn't exist" do + FileUtils.rm_rf(ssh_dir) + + is_expected.to be_truthy + end + + it 'succeeds if all the whitelisted files exist' do + described_class::WHITELIST.each do |filename| + stub_ssh_file(filename) + end + + is_expected.to be_truthy + end + end + + def stub_user + allow(File).to receive(:expand_path).with("~#{username}").and_return(home_dir) + end + + def stub_home_dir + FileUtils.mkdir_p(home_dir) + end + + def stub_ssh_file(filename) + FileUtils.mkdir_p(ssh_dir) + FileUtils.touch(File.join(ssh_dir, filename)) + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b84e3ff18e8..84656ffe0b9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -546,6 +546,22 @@ describe Ci::Pipeline, :mailer do end end + describe '#has_kubernetes_active?' do + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + + it 'returns true' do + expect(pipeline).to have_kubernetes_active + end + end + + context 'when kubernetes is not active' do + it 'returns false' do + expect(pipeline).not_to have_kubernetes_active + end + end + end + describe '#has_stage_seeds?' do context 'when pipeline has stage seeds' do subject { build(:ci_pipeline_with_one_job) } diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index b1743cd608e..537cdadd528 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -203,18 +203,13 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe '#predefined_variables' do let(:kubeconfig) do - config = - YAML.load(File.read(expand_fixture_path('config/kubeconfig.yml'))) - - config.dig('users', 0, 'user')['token'] = - 'token' - + config_file = expand_fixture_path('config/kubeconfig.yml') + config = YAML.load(File.read(config_file)) + config.dig('users', 0, 'user')['token'] = 'token' + config.dig('contexts', 0, 'context')['namespace'] = namespace config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = Base64.encode64('CA PEM DATA') - config.dig('contexts', 0, 'context')['namespace'] = - namespace - YAML.dump(config) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 40875c8fb7e..7065d467ec0 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -886,7 +886,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(committer, repository, old_rev, blank_sha, 'refs/heads/feature') + .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -932,20 +932,20 @@ describe Repository, models: true do service = Gitlab::Git::HooksService.new expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with(committer, target_repository, old_rev, new_rev, updating_ref) + .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref) .and_yield(service).and_return(true) end it 'runs without errors' do expect do - GitOperationService.new(committer, repository).with_branch('feature') do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do new_rev end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - service = GitOperationService.new(committer, repository) + service = Gitlab::Git::OperationService.new(committer, repository.raw_repository) expect(service).to receive(:update_autocrlf_option) @@ -956,7 +956,7 @@ describe Repository, models: true do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - GitOperationService.new(committer, repository).with_branch('feature') do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do new_rev end @@ -971,13 +971,13 @@ describe Repository, models: true do let(:updating_ref) { 'refs/heads/master' } it 'fetch_ref and create the branch' do - expect(target_project.repository).to receive(:fetch_ref) + expect(target_project.repository.raw_repository).to receive(:fetch_ref) .and_call_original - GitOperationService.new(committer, target_repository) + Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) .with_branch( 'master', - start_project: project, + start_repository: project.repository.raw_repository, start_branch_name: 'feature') { new_rev } expect(target_repository.branch_names).to contain_exactly('master') @@ -990,8 +990,8 @@ describe Repository, models: true do it 'does not fetch_ref and just pass the commit' do expect(target_repository).not_to receive(:fetch_ref) - GitOperationService.new(committer, target_repository) - .with_branch('feature', start_project: project) { new_rev } + Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev } end end end @@ -1000,7 +1000,7 @@ describe Repository, models: true do let(:target_project) { create(:project, :empty_repo) } before do - expect(target_project.repository).to receive(:run_git) + expect(target_project.repository.raw_repository).to receive(:run_git) end it 'raises Rugged::ReferenceError' do @@ -1009,9 +1009,9 @@ describe Repository, models: true do end expect do - GitOperationService.new(committer, target_project.repository) + Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository) .with_branch('feature', - start_project: project, + start_repository: project.repository.raw_repository, &:itself) end.to raise_reference_error end @@ -1031,7 +1031,7 @@ describe Repository, models: true do repository.add_branch(user, branch, old_rev) expect do - GitOperationService.new(committer, repository).with_branch(branch) do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do new_rev end end.not_to raise_error @@ -1049,10 +1049,10 @@ describe Repository, models: true do # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - GitOperationService.new(committer, repository).with_branch(branch) do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do new_rev end - end.to raise_error(Repository::CommitError) + end.to raise_error(Gitlab::Git::CommitError) end end @@ -1061,7 +1061,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - GitOperationService.new(committer, repository).with_branch('feature') do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do new_rev end end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) @@ -1079,10 +1079,9 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - GitOperationService.new(committer, repository) - .with_branch('new-feature') do - new_rev - end + repository.with_branch(user, 'new-feature') do + new_rev + end end end @@ -1139,7 +1138,7 @@ describe Repository, models: true do describe 'when there are no branches' do before do - allow(repository).to receive(:branch_count).and_return(0) + allow(repository.raw_repository).to receive(:branch_count).and_return(0) end it { is_expected.to eq(false) } @@ -1147,7 +1146,7 @@ describe Repository, models: true do describe 'when there are branches' do it 'returns true' do - expect(repository).to receive(:branch_count).and_return(3) + expect(repository.raw_repository).to receive(:branch_count).and_return(3) expect(subject).to eq(true) end @@ -1161,7 +1160,7 @@ describe Repository, models: true do end it 'sets autocrlf to :input' do - GitOperationService.new(nil, repository).send(:update_autocrlf_option) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option) expect(repository.raw_repository.autocrlf).to eq(:input) end @@ -1176,7 +1175,7 @@ describe Repository, models: true do expect(repository.raw_repository).not_to receive(:autocrlf=) .with(:input) - GitOperationService.new(nil, repository).send(:update_autocrlf_option) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option) end end end @@ -1762,15 +1761,15 @@ describe Repository, models: true do describe '#update_ref' do it 'can create a ref' do - GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) - end.to raise_error(Repository::CommitError) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + end.to raise_error(Gitlab::Git::CommitError) end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 971eaf837cb..114019441a3 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -224,7 +224,7 @@ describe API::Files do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file) - .and_raise(Repository::CommitError, 'Cannot create file') + .and_raise(Gitlab::Git::CommitError, 'Cannot create file') post api(route("any%2Etxt"), user), valid_params @@ -339,7 +339,7 @@ describe API::Files do end it "returns a 400 if fails to delete file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') delete api(route(file_path), user), valid_params diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 4ffa5d1784e..dc7f0eefd16 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -127,7 +127,7 @@ describe API::V3::Files do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file) - .and_raise(Repository::CommitError, 'Cannot create file') + .and_raise(Gitlab::Git::CommitError, 'Cannot create file') post v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -228,7 +228,7 @@ describe API::V3::Files do end it "returns a 400 if fails to delete file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') delete v3_api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb index 3390ae247ff..f2c19c7642a 100644 --- a/spec/views/ci/lints/show.html.haml_spec.rb +++ b/spec/views/ci/lints/show.html.haml_spec.rb @@ -73,8 +73,8 @@ describe 'ci/lints/show' do render expect(rendered).to have_content('Tag list: dotnet') - expect(rendered).to have_content('Refs only: test@dude/repo') - expect(rendered).to have_content('Refs except: deploy') + expect(rendered).to have_content('Only policy: refs, test@dude/repo') + expect(rendered).to have_content('Except policy: refs, deploy') expect(rendered).to have_content('Environment: testing') expect(rendered).to have_content('When: on_success') end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 05f971dfd13..c4979792194 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -23,8 +23,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original - expect_any_instance_of(Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id) end @@ -143,7 +143,7 @@ describe GitGarbageCollectWorker do tree: old_commit.tree, parents: [old_commit] ) - GitOperationService.new(nil, project.repository).send( + Gitlab::Git::OperationService.new(nil, project.repository.raw_repository).send( :update_ref, "refs/heads/#{SecureRandom.hex(6)}", new_commit_sha, |