diff options
65 files changed, 1091 insertions, 779 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index afed694eede..e40e4fc339c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.65.0 +0.66.0 diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index d918d80df8d..df20e1e9c88 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -57,12 +57,12 @@ class GfmAutoComplete { displayTpl(value) { if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; // eslint-disable-next-line no-template-curly-in-string - let tpl = '<li>/${name}'; + let tpl = '<li><span class="name">/${name}</span>'; if (value.aliases.length > 0) { - tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; + tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>'; } if (value.params.length > 0) { - tpl += ' <small><%- params.join(" ") %></small>'; + tpl += ' <small class="params"><%- params.join(" ") %></small>'; } if (value.description !== '') { tpl += '<small class="description"><i><%- description %></i></small>'; diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 02129d39846..42e79a9e17a 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -141,7 +141,8 @@ export default { <div v-if="group.description" class="description"> - {{group.description}} + <span v-html="group.description"> + </span> </div> </div> <group-folder diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index ffc86175548..4a7569078a1 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -71,7 +71,7 @@ export default class GroupsStore { id: rawGroupItem.id, name: rawGroupItem.name, fullName: rawGroupItem.full_name, - description: rawGroupItem.description, + description: rawGroupItem.markdown_description, visibility: rawGroupItem.visibility, avatarUrl: rawGroupItem.avatar_url, relativePath: rawGroupItem.relative_path, diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index eede04a06cd..a50b80c23d0 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -69,8 +69,8 @@ currentFlagPosition: 0, showFlag: false, showFlagContent: false, - showDeployInfo: true, timeSeries: [], + realPixelRatio: 1, }; }, @@ -87,10 +87,7 @@ }, innerViewBox() { - if ((this.baseGraphWidth - 150) > 0) { - return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; - } - return '0 0 0 0'; + return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; }, axisTransform() { @@ -102,6 +99,10 @@ paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, }; }, + + deploymentFlagData() { + return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); + }, }, methods: { @@ -122,6 +123,10 @@ this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.baseGraphHeight = this.graphHeight; this.baseGraphWidth = this.graphWidth; + + // pixel offsets inside the svg and outside are not 1:1 + this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth); + this.renderAxesPaths(); this.formatDeployments(); }, @@ -261,6 +266,11 @@ :line-color="path.lineColor" :area-color="path.areaColor" /> + <graph-deployment + :deployment-data="reducedDeploymentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> <rect class="prometheus-graph-overlay" :width="(graphWidth - 70)" @@ -269,24 +279,21 @@ ref="graphOverlay" @mousemove="handleMouseOverGraph($event)"> </rect> - <graph-deployment - :show-deploy-info="showDeployInfo" - :deployment-data="reducedDeploymentData" - :graph-width="graphWidth" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - /> - <graph-flag - v-if="showFlag" - :current-x-coordinate="currentXCoordinate" - :current-data="currentData" - :current-flag-position="currentFlagPosition" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - :show-flag-content="showFlagContent" - /> </svg> </svg> + <graph-flag + :real-pixel-ratio="realPixelRatio" + :current-x-coordinate="currentXCoordinate" + :current-data="currentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + :show-flag-content="showFlagContent" + :time-series="timeSeries" + :unit-of-display="unitOfDisplay" + :current-data-index="currentDataIndex" + :legend-title="legendTitle" + :deployment-flag-data="deploymentFlagData" + /> </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 026e2fd0c49..8d6393d4ce5 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -1,13 +1,6 @@ <script> - import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters'; - import Icon from '../../../vue_shared/components/icon.vue'; - export default { props: { - showDeployInfo: { - type: Boolean, - required: true, - }, deploymentData: { type: Array, required: true, @@ -20,14 +13,6 @@ type: Number, required: true, }, - graphWidth: { - type: Number, - required: true, - }, - }, - - components: { - Icon, }, computed: { @@ -37,52 +22,17 @@ }, methods: { - refText(d) { - return d.tag ? d.ref : d.sha.slice(0, 8); - }, - - formatTime(deploymentTime) { - return timeFormat(deploymentTime); - }, - - formatDate(deploymentTime) { - return dateFormatWithName(deploymentTime); - }, - - nameDeploymentClass(deployment) { - return `deploy-info-${deployment.id}`; - }, - transformDeploymentGroup(deployment) { - return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; - }, - - positionFlag(deployment) { - let xPosition = 3; - if (deployment.xPos > (this.graphWidth - 225)) { - xPosition = -142; - } - return xPosition; - }, - - svgContainerHeight(tag) { - let svgHeight = 80; - if (!tag) { - svgHeight -= 20; - } - return svgHeight; + return `translate(${Math.floor(deployment.xPos) - 5}, 20)`; }, }, }; </script> <template> - <g - class="deploy-info" - v-if="showDeployInfo"> + <g class="deploy-info"> <g v-for="(deployment, index) in deploymentData" :key="index" - :class="nameDeploymentClass(deployment)" :transform="transformDeploymentGroup(deployment)"> <rect x="0" @@ -99,81 +49,6 @@ :y2="calculatedHeight" stroke="#000"> </line> - <svg - v-if="deployment.showDeploymentFlag" - class="js-deploy-info-box" - :x="positionFlag(deployment)" - y="0" - width="134" - :height="svgContainerHeight(deployment.tag)"> - <rect - class="rect-text-metric deploy-info-rect rect-metric" - x="1" - y="1" - rx="2" - width="132" - :height="svgContainerHeight(deployment.tag) - 2"> - </rect> - <text - class="deploy-info-text text-metric-bold" - transform="translate(5, 2)"> - Deployed - </text> - <!--The date info--> - <g transform="translate(5, 20)"> - <text class="deploy-info-text"> - {{formatDate(deployment.time)}} - </text> - <text - class="deploy-info-text text-metric-bold" - x="62"> - {{formatTime(deployment.time)}} - </text> - </g> - <line - class="divider-line" - x1="0" - y1="38" - x2="132" - :y2="38" - stroke="#000"> - </line> - <!--Commit information--> - <g transform="translate(5, 40)"> - <icon - name="commit" - :width="12" - :height="12" - :y="3"> - </icon> - <a :xlink:href="deployment.commitUrl"> - <text - class="deploy-info-text deploy-info-text-link" - transform="translate(20, 2)"> - {{refText(deployment)}} - </text> - </a> - </g> - <!--Tag information--> - <g - transform="translate(5, 55)" - v-if="deployment.tag"> - <icon - name="label" - :width="12" - :height="12" - :y="5"> - </icon> - <a :xlink:href="deployment.tagUrl"> - <text - class="deploy-info-text deploy-info-text-link" - transform="translate(20, 2)" - y="2"> - {{deployment.tag}} - </text> - </a> - </g> - </svg> </g> <svg height="0" diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 10fb7ff6803..62ebc3f419c 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -1,5 +1,7 @@ <script> import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; + import { formatRelevantDigits } from '../../../lib/utils/number_utils'; + import Icon from '../../../vue_shared/components/icon.vue'; export default { props: { @@ -7,14 +9,15 @@ type: Number, required: true, }, - currentFlagPosition: { - type: Number, - required: true, - }, currentData: { type: Object, required: true, }, + deploymentFlagData: { + type: Object, + required: false, + default: null, + }, graphHeight: { type: Number, required: true, @@ -23,71 +26,173 @@ type: Number, required: true, }, + realPixelRatio: { + type: Number, + required: true, + }, showFlagContent: { type: Boolean, required: true, }, + timeSeries: { + type: Array, + required: true, + }, + unitOfDisplay: { + type: String, + required: true, + }, + currentDataIndex: { + type: Number, + required: true, + }, + legendTitle: { + type: String, + required: true, + }, }, - data() { - return { - circleColorRgb: '#8fbce8', - }; + components: { + Icon, }, computed: { formatTime() { - return timeFormat(this.currentData.time); + return this.deploymentFlagData ? + timeFormat(this.deploymentFlagData.time) : + timeFormat(this.currentData.time); }, formatDate() { - return dateFormat(this.currentData.time); + return this.deploymentFlagData ? + dateFormat(this.deploymentFlagData.time) : + dateFormat(this.currentData.time); + }, + + cursorStyle() { + const xCoordinate = this.deploymentFlagData ? + this.deploymentFlagData.xPos : + this.currentXCoordinate; + + const offsetTop = 20 * this.realPixelRatio; + const offsetLeft = (70 + xCoordinate) * this.realPixelRatio; + const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio; + + return { + top: `${offsetTop}px`, + left: `${offsetLeft}px`, + height: `${height}px`, + }; + }, + + flagOrientation() { + if (this.currentXCoordinate * this.realPixelRatio > 120) { + return 'left'; + } + return 'right'; + }, + }, + + methods: { + seriesMetricValue(series) { + const index = this.deploymentFlagData ? + this.deploymentFlagData.seriesIndex : + this.currentDataIndex; + const value = series.values[index] && + series.values[index].value; + if (isNaN(value)) { + return '-'; + } + return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; }, - calculatedHeight() { - return this.graphHeight - this.graphHeightOffset; + seriesMetricLabel(index, series) { + if (this.timeSeries.length < 2) { + return this.legendTitle; + } + if (series.metricTag) { + return series.metricTag; + } + return `series ${index + 1}`; + }, + + strokeDashArray(type) { + if (type === 'dashed') return '6, 3'; + if (type === 'dotted') return '3, 3'; + return null; }, }, }; </script> + <template> - <g class="mouse-over-flag"> - <line - class="selected-metric-line" - :x1="currentXCoordinate" - :y1="0" - :x2="currentXCoordinate" - :y2="calculatedHeight" - transform="translate(-5, 20)"> - </line> - <svg + <div + class="prometheus-graph-cursor" + :style="cursorStyle" + > + <div v-if="showFlagContent" - class="rect-text-metric" - :x="currentFlagPosition" - y="0"> - <rect - class="rect-metric" - x="4" - y="1" - rx="2" - width="90" - height="40" - transform="translate(-3, 20)"> - </rect> - <text - class="text-metric text-metric-bold" - x="16" - y="35" - transform="translate(-5, 20)"> - {{formatTime}} - </text> - <text - class="text-metric" - x="16" - y="15" - transform="translate(-5, 20)"> - {{formatDate}} - </text> - </svg> - </g> + class="prometheus-graph-flag popover" + :class="flagOrientation" + > + <div class="arrow"></div> + <div class="popover-title"> + <h5 v-if="this.deploymentFlagData"> + Deployed + </h5> + {{formatDate}} at + <strong>{{formatTime}}</strong> + </div> + <div + v-if="this.deploymentFlagData" + class="popover-content deploy-meta-content" + > + <div> + <icon + name="commit" + :size="12"> + </icon> + <a :href="deploymentFlagData.commitUrl"> + {{deploymentFlagData.sha.slice(0, 8)}} + </a> + </div> + <div + v-if="deploymentFlagData.tag"> + <icon + name="label" + :size="12"> + </icon> + <a :href="deploymentFlagData.tagUrl"> + {{deploymentFlagData.ref}} + </a> + </div> + </div> + <div class="popover-content"> + <table> + <tr + v-for="(series, index) in timeSeries" + :key="index" + > + <td> + <svg width="15" height="6"> + <line + :stroke="series.lineColor" + :stroke-dasharray="strokeDashArray(series.lineStyle)" + stroke-width="4" + x1="0" + x2="15" + y1="2" + y2="2"> + </line> + </svg> + </td> + <td>{{seriesMetricLabel(index, series)}}</td> + <td> + <strong>{{seriesMetricValue(series)}}</strong> + </td> + </tr> + </table> + </div> + </div> + </div> </template> diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index cbca14ede02..6cc67ba57ee 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -29,15 +29,18 @@ const mixins = { time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); if (xPos >= 0) { + const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1); + deploymentDataArray.push({ id: deployment.id, time, sha: deployment.sha, commitUrl: `${this.projectPath}/commit/${deployment.sha}`, tag: deployment.tag, - tagUrl: `${this.tagsPath}/${deployment.tag}`, + tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null, ref: deployment.ref.name, xPos, + seriesIndex, showDeploymentFlag: false, }); } diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js index 068813ddee6..f3c9acdd93e 100644 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js @@ -14,7 +14,7 @@ const d3 = { timeYear, }; -export const dateFormat = d3.time('%b %-d, %Y'); +export const dateFormat = d3.time('%a, %b %-d'); export const timeFormat = d3.time('%-I:%M%p'); export const dateFormatWithName = d3.time('%a, %b %-d'); export const bisectDate = d3.bisector(d => d.time).left; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 759cc9925f4..f249bd036d6 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -541,7 +541,6 @@ function UsersSelect(currentUser, els, options = {}) { options.projectId = $(select).data('project-id'); options.groupId = $(select).data('group-id'); options.showCurrentUser = $(select).data('current-user'); - options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches'); options.authorId = $(select).data('author-id'); options.skipUsers = $(select).data('skip-users'); showNullUser = $(select).data('null-user'); @@ -688,7 +687,6 @@ UsersSelect.prototype.users = function(query, options, callback) { todo_filter: options.todoFilter || null, todo_state_filter: options.todoStateFilter || null, current_user: options.showCurrentUser || null, - push_code_to_protected_branches: options.pushCodeToProtectedBranches || null, author_id: options.authorId || null, skip_users: options.skipUsers || null }, diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 6b07ffdbd61..938f5f49c09 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -192,6 +192,17 @@ overflow-y: auto; overflow-x: hidden; + .name, + small.aliases, + small.params { + float: left; + } + + small.aliases, + small.params { + padding: 2px 5px; + } + small.description { float: right; padding: 3px 5px; @@ -209,6 +220,7 @@ } ul > li { + @include clearfix; white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index f4882305c57..794bc668562 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -248,6 +248,73 @@ } } +.prometheus-graph-cursor { + position: absolute; + background: $theme-gray-600; + width: 1px; +} + +.prometheus-graph-flag { + display: block; + min-width: 160px; + + h5 { + padding: 0; + margin: 0; + font-size: 14px; + line-height: 1.2; + } + + table { + border-collapse: collapse; + padding: 0; + margin: 0; + } + + td { + vertical-align: middle; + + + td { + padding-left: 5px; + vertical-align: top; + } + } + + .deploy-meta-content { + border-bottom: 1px solid $white-dark; + + svg { + height: 15px; + vertical-align: bottom; + } + } + + &.popover { + &.left { + left: auto; + right: 0; + margin-right: 10px; + } + + &.right { + left: 0; + right: auto; + margin-left: 10px; + } + + > .arrow { + top: 40px; + } + + > .popover-title, + > .popover-content { + padding: 5px 8px; + font-size: 12px; + white-space: nowrap; + } + } +} + .prometheus-svg-container { position: relative; height: 0; diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 9e79852e378..6025a40348b 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController def require_pages_enabled! not_found unless @project.pages_available? end + + def check_issues_available! + return render_404 unless @project.feature_available?(:issues, current_user) + end end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index e36105ddc11..949e54ff819 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -2,6 +2,7 @@ class Projects::BoardsController < Projects::ApplicationController include BoardsResponses include IssuableCollections + before_action :check_issues_available! before_action :authorize_read_board!, only: [:index, :show] before_action :assign_endpoint_vars diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d7a3441a245..384f18b316c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -194,10 +194,6 @@ class Projects::IssuesController < Projects::ApplicationController render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) end - def check_issues_available! - return render_404 unless @project.feature_available?(:issues, current_user) - end - def render_issue_json if @issue.valid? render json: serializer.represent(@issue) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 1a4f1431bdc..6cefcde558a 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -73,7 +73,6 @@ module SelectsHelper email_user: opts[:email_user] || false, first_user: opts[:first_user] && current_user ? current_user.username : false, current_user: opts[:current_user] || false, - "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], author_id: opts[:author_id] || '', skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil } diff --git a/app/models/commit.rb b/app/models/commit.rb index 2be07ca7d3c..39d7f5b159d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -371,7 +371,7 @@ class Commit # # Returns a symbol def uri_type(path) - entry = @raw.tree.path(path) + entry = @raw.rugged_tree_entry(path) if entry[:type] == :blob blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob.image? || blob.video? ? :raw : :blob diff --git a/app/models/project.rb b/app/models/project.rb index 4cb9d9fe637..fbe65e700a4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1450,6 +1450,7 @@ class Project < ActiveRecord::Base import_finish remove_import_jid update_project_counter_caches + after_create_default_branch end def update_project_counter_caches @@ -1463,6 +1464,27 @@ class Project < ActiveRecord::Base end end + def after_create_default_branch + return unless default_branch + + # Ensure HEAD points to the default branch in case it is not master + change_head(default_branch) + + if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) + params = { + name: default_branch, + push_access_levels_attributes: [{ + access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + }], + merge_access_levels_attributes: [{ + access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + }] + } + + ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true) + end + end + def remove_import_jid return unless import_jid diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f599eab42f2..1dd8f0a25a9 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -241,7 +241,6 @@ class ProjectPolicy < BasePolicy rule { repository_disabled }.policy do prevent :push_code - prevent :push_code_to_protected_branches prevent :download_code prevent :fork_project prevent :read_commit_status diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb index 37240bfb0b1..aca4e4ca488 100644 --- a/app/serializers/group_child_entity.rb +++ b/app/serializers/group_child_entity.rb @@ -1,6 +1,7 @@ class GroupChildEntity < Grape::Entity include ActionView::Helpers::NumberHelper include RequestAwareEntity + include MarkupHelper expose :id, :name, :description, :visibility, :full_name, :created_at, :updated_at, :avatar_url @@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity number_with_delimiter(instance.member_count) end + expose :markdown_description do |instance| + markdown_description + end + private def membership @@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity def type object.class.name.downcase end + + def markdown_description + markdown_field(object, :description) + end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bb61136e33b..e6fd193ffb3 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -154,24 +154,7 @@ class GitPushService < BaseService offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) - # Ensure HEAD points to the default branch in case it is not master - project.change_head(branch_name) - - # Set protection on the default branch if configured - if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch) - - params = { - name: @project.default_branch, - push_access_levels_attributes: [{ - access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }], - merge_access_levels_attributes: [{ - access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }] - } - - ProtectedBranches::CreateService.new(@project, current_user, params).execute - end + @project.after_create_default_branch end def build_push_data diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index a84e335340d..6212fd69077 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -2,8 +2,8 @@ module ProtectedBranches class CreateService < BaseService attr_reader :protected_branch - def execute - raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + def execute(skip_authorization: false) + raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project) project.protected_branches.create(params) end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index be39f577ba7..1fa3a3041fd 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -299,9 +299,10 @@ Charts -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue + - if project_nav_tab?(:issues) + %li.hidden + = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do + Create a new issue -# Shortcut to Pipelines > Jobs - if project_nav_tab? :builds @@ -316,5 +317,6 @@ Commits -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + - if project_nav_tab?(:issues) + %li.hidden + = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb index aeb3bc019b9..376703f6319 100644 --- a/app/workers/background_migration_worker.rb +++ b/app/workers/background_migration_worker.rb @@ -1,10 +1,53 @@ class BackgroundMigrationWorker include ApplicationWorker + # The minimum amount of time between processing two jobs of the same migration + # class. + # + # This interval is set to 5 minutes so autovacuuming and other maintenance + # related tasks have plenty of time to clean up after a migration has been + # performed. + MIN_INTERVAL = 5.minutes.to_i + # Performs the background migration. # # See Gitlab::BackgroundMigration.perform for more information. + # + # class_name - The class name of the background migration to run. + # arguments - The arguments to pass to the migration class. def perform(class_name, arguments = []) - Gitlab::BackgroundMigration.perform(class_name, arguments) + should_perform, ttl = perform_and_ttl(class_name) + + if should_perform + Gitlab::BackgroundMigration.perform(class_name, arguments) + else + # If the lease could not be obtained this means either another process is + # running a migration of this class or we ran one recently. In this case + # we'll reschedule the job in such a way that it is picked up again around + # the time the lease expires. + self.class.perform_in(ttl || MIN_INTERVAL, class_name, arguments) + end + end + + def perform_and_ttl(class_name) + if always_perform? + # In test environments `perform_in` will run right away. This can then + # lead to stack level errors in the above `#perform`. To work around this + # we'll just perform the migration right away in the test environment. + [true, nil] + else + lease = lease_for(class_name) + + [lease.try_obtain, lease.ttl] + end + end + + def lease_for(class_name) + Gitlab::ExclusiveLease + .new("#{self.class.name}:#{class_name}", timeout: MIN_INTERVAL) + end + + def always_perform? + Rails.env.test? end end diff --git a/changelogs/unreleased/38030-add-graph-value-to-hover.yml b/changelogs/unreleased/38030-add-graph-value-to-hover.yml new file mode 100644 index 00000000000..233db2b19c9 --- /dev/null +++ b/changelogs/unreleased/38030-add-graph-value-to-hover.yml @@ -0,0 +1,5 @@ +--- +title: Display graph values on hover within monitoring page +merge_request: 16261 +author: +type: changed diff --git a/changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml b/changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml new file mode 100644 index 00000000000..e972ac6d54a --- /dev/null +++ b/changelogs/unreleased/3968-protected-branch-is-not-set-for-default-branch-on-import.yml @@ -0,0 +1,5 @@ +--- +title: Protected branch is now created for default branch on import +merge_request: 16198 +author: +type: fixed diff --git a/changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml b/changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml new file mode 100644 index 00000000000..71a606ff607 --- /dev/null +++ b/changelogs/unreleased/40190-fix-slash-commands-dropdown-description-mis-alignement-on-firefox.yml @@ -0,0 +1,5 @@ +--- +title: "Fix slash commands dropdown description mis-alignment on Firefox" +merge_request: 16125 +author: Maurizio De Santis +type: fixed diff --git a/changelogs/unreleased/40549-render-emoj-in-groups-overview.yml b/changelogs/unreleased/40549-render-emoj-in-groups-overview.yml new file mode 100644 index 00000000000..9b2f58df440 --- /dev/null +++ b/changelogs/unreleased/40549-render-emoj-in-groups-overview.yml @@ -0,0 +1,5 @@ +--- +title: Rendering of emoji's in Group-Overview +merge_request: 16098 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml b/changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml new file mode 100644 index 00000000000..b2c3a86551b --- /dev/null +++ b/changelogs/unreleased/41244-issue-board-shortcut-working-while-no-issues.yml @@ -0,0 +1,5 @@ +--- +title: disables shortcut to issue boards when issues are not enabled +merge_request: 16020 +author: Christiaan Van den Poel +type: fixed diff --git a/changelogs/unreleased/delay-background-migrations.yml b/changelogs/unreleased/delay-background-migrations.yml new file mode 100644 index 00000000000..aa12591e7d2 --- /dev/null +++ b/changelogs/unreleased/delay-background-migrations.yml @@ -0,0 +1,5 @@ +--- +title: Run background migrations with a minimum interval +merge_request: +author: +type: changed diff --git a/features/explore/groups.feature b/features/explore/groups.feature deleted file mode 100644 index 830810615e0..00000000000 --- a/features/explore/groups.feature +++ /dev/null @@ -1,105 +0,0 @@ -@public -Feature: Explore Groups - Background: - Given group "TestGroup" has private project "Enterprise" - - @javascript - Scenario: I should see group with private and internal projects as user - Given group "TestGroup" has internal project "Internal" - When I sign in as a user - And I visit group "TestGroup" page - Then I should see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group issues for internal project as user - Given group "TestGroup" has internal project "Internal" - When I sign in as a user - And I visit group "TestGroup" issues page - Then I should see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group merge requests for internal project as user - Given group "TestGroup" has internal project "Internal" - When I sign in as a user - And I visit group "TestGroup" merge requests page - Then I should see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group with private, internal and public projects as visitor - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - When I visit group "TestGroup" page - Then I should see project "Community" items - And I should not see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group issues for public project as visitor - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - When I visit group "TestGroup" issues page - Then I should see project "Community" items - And I should not see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group merge requests for public project as visitor - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - When I visit group "TestGroup" merge requests page - Then I should see project "Community" items - And I should not see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group with private, internal and public projects as user - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - When I sign in as a user - And I visit group "TestGroup" page - Then I should see project "Community" items - And I should see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group issues for internal and public projects as user - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - When I sign in as a user - And I visit group "TestGroup" issues page - Then I should see project "Community" items - And I should see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group merge requests for internal and public projects as user - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - When I sign in as a user - And I visit group "TestGroup" merge requests page - Then I should see project "Community" items - And I should see project "Internal" items - And I should not see project "Enterprise" items - - @javascript - Scenario: I should see group with public project in public groups area - Given group "TestGroup" has public project "Community" - When I visit the public groups area - Then I should see group "TestGroup" - - @javascript - Scenario: I should see group with public project in public groups area as user - Given group "TestGroup" has public project "Community" - When I sign in as a user - And I visit the public groups area - Then I should see group "TestGroup" - - @javascript - Scenario: I should see group with internal project in public groups area as user - Given group "TestGroup" has internal project "Internal" - When I sign in as a user - And I visit the public groups area - Then I should see group "TestGroup" diff --git a/features/invites.feature b/features/invites.feature deleted file mode 100644 index dc8eefaeaed..00000000000 --- a/features/invites.feature +++ /dev/null @@ -1,45 +0,0 @@ -Feature: Invites - Background: - Given "John Doe" is owner of group "Owned" - And "John Doe" has invited "user@example.com" to group "Owned" - - Scenario: Viewing invitation when signed out - When I visit the invitation page - Then I should be redirected to the sign in page - And I should see a notice telling me to sign in - - Scenario: Signing in to view invitation - When I visit the invitation page - And I sign in as "Mary Jane" - Then I should be redirected to the invitation page - - Scenario: Viewing invitation when signed in - Given I sign in as "Mary Jane" - And I visit the invitation page - Then I should see the invitation details - And I should see an "Accept invitation" button - And I should see a "Decline" button - - Scenario: Viewing invitation as an existing member - Given I sign in as "John Doe" - And I visit the invitation page - Then I should see a message telling me I'm already a member - - Scenario: Accepting the invitation - Given I sign in as "Mary Jane" - And I visit the invitation page - And I click the "Accept invitation" button - Then I should be redirected to the group page - And I should see a notice telling me I have access - - Scenario: Declining the application when signed in - Given I sign in as "Mary Jane" - And I visit the invitation page - And I click the "Decline" button - Then I should be redirected to the dashboard - And I should see a notice telling me I have declined - - Scenario: Declining the application when signed out - When I visit the invitation's decline page - Then I should be redirected to the sign in page - And I should see a notice telling me I have declined diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb deleted file mode 100644 index 409bf0cb416..00000000000 --- a/features/steps/explore/groups.rb +++ /dev/null @@ -1,88 +0,0 @@ -class Spinach::Features::ExploreGroups < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedGroup - include SharedProject - - step 'group "TestGroup" has private project "Enterprise"' do - group_has_project("TestGroup", "Enterprise", Gitlab::VisibilityLevel::PRIVATE) - end - - step 'group "TestGroup" has internal project "Internal"' do - group_has_project("TestGroup", "Internal", Gitlab::VisibilityLevel::INTERNAL) - end - - step 'group "TestGroup" has public project "Community"' do - group_has_project("TestGroup", "Community", Gitlab::VisibilityLevel::PUBLIC) - end - - step '"John Doe" is owner of group "TestGroup"' do - group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup") - user = create(:user, name: "John Doe") - group.add_owner(user) - end - - step 'I visit group "TestGroup" page' do - visit group_path(Group.find_by(name: "TestGroup")) - end - - step 'I visit group "TestGroup" issues page' do - visit issues_group_path(Group.find_by(name: "TestGroup")) - end - - step 'I visit group "TestGroup" merge requests page' do - visit merge_requests_group_path(Group.find_by(name: "TestGroup")) - end - - step 'I visit group "TestGroup" members page' do - visit group_group_members_path(Group.find_by(name: "TestGroup")) - end - - step 'I should not see project "Enterprise" items' do - expect(page).not_to have_content "Enterprise" - end - - step 'I should see project "Internal" items' do - expect(page).to have_content "Internal" - end - - step 'I should not see project "Internal" items' do - expect(page).not_to have_content "Internal" - end - - step 'I should see project "Community" items' do - expect(page).to have_content "Community" - end - - step 'I change filter to Everyone\'s' do - click_link "Everyone's" - end - - step 'I should see group member "John Doe"' do - expect(page).to have_content "John Doe" - end - - protected - - def group_has_project(groupname, projectname, visibility_level) - group = Group.find_by(name: groupname) || create(:group, name: groupname) - project = create(:project, - namespace: group, - name: projectname, - path: "#{groupname}-#{projectname}", - visibility_level: visibility_level - ) - create(:issue, - title: "#{projectname} feature", - project: project - ) - create(:merge_request, - title: "#{projectname} feature implemented", - source_project: project, - target_project: project - ) - create(:closed_issue_event, - project: project - ) - end -end diff --git a/features/steps/invites.rb b/features/steps/invites.rb deleted file mode 100644 index dac972172aa..00000000000 --- a/features/steps/invites.rb +++ /dev/null @@ -1,80 +0,0 @@ -class Spinach::Features::Invites < Spinach::FeatureSteps - include SharedAuthentication - include SharedUser - include SharedGroup - - step '"John Doe" has invited "user@example.com" to group "Owned"' do - user = User.find_by(name: "John Doe") - group = Group.find_by(name: "Owned") - group.add_developer("user@example.com", user) - end - - step 'I visit the invitation page' do - group = Group.find_by(name: "Owned") - invite = group.group_members.invite.last - invite.generate_invite_token! - @raw_invite_token = invite.raw_invite_token - visit invite_path(@raw_invite_token) - end - - step 'I should be redirected to the sign in page' do - expect(current_path).to eq(new_user_session_path) - end - - step 'I should see a notice telling me to sign in' do - expect(page).to have_content "To accept this invitation, sign in" - end - - step 'I should be redirected to the invitation page' do - expect(current_path).to eq(invite_path(@raw_invite_token)) - end - - step 'I should see the invitation details' do - expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.") - end - - step "I should see a message telling me I'm already a member" do - expect(page).to have_content("However, you are already a member of this group.") - end - - step 'I should see an "Accept invitation" button' do - expect(page).to have_link("Accept invitation") - end - - step 'I should see a "Decline" button' do - expect(page).to have_link("Decline") - end - - step 'I click the "Accept invitation" button' do - page.click_link "Accept invitation" - end - - step 'I should be redirected to the group page' do - group = Group.find_by(name: "Owned") - expect(current_path).to eq(group_path(group)) - end - - step 'I should see a notice telling me I have access' do - expect(page).to have_content("You have been granted Developer access to group Owned.") - end - - step 'I click the "Decline" button' do - page.click_link "Decline" - end - - step 'I should be redirected to the dashboard' do - expect(current_path).to eq(dashboard_projects_path) - end - - step 'I should see a notice telling me I have declined' do - expect(page).to have_content("You have declined the invitation to join group Owned.") - end - - step "I visit the invitation's decline page" do - group = Group.find_by(name: "Owned") - invite = group.group_members.invite.last - invite.generate_invite_token! - @raw_invite_token = invite.raw_invite_token - visit decline_invite_path(@raw_invite_token) - end -end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 33171f83692..7b35c24d153 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -842,6 +842,12 @@ into similar problems in the future (e.g. when new tables are created). def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE) raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id') + # To not overload the worker too much we enforce a minimum interval both + # when scheduling and performing jobs. + if delay_interval < BackgroundMigrationWorker::MIN_INTERVAL + delay_interval = BackgroundMigrationWorker::MIN_INTERVAL + end + model_class.each_batch(of: batch_size) do |relation, index| start_id, end_id = relation.pluck('MIN(id), MAX(id)').first diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 3f7b42456af..dbb8f317afe 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -71,5 +71,16 @@ module Gitlab redis.exists(@redis_shared_state_key) end end + + # Returns the TTL of the Redis key. + # + # This method will return `nil` if no TTL could be obtained. + def ttl + Gitlab::Redis::SharedState.with do |redis| + ttl = redis.ttl(@redis_shared_state_key) + + ttl if ttl.positive? + end + end end end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index a1755143abe..031fccba92b 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -173,8 +173,8 @@ module Gitlab end def find_by_rugged(repository, sha, path, limit:) - commit = repository.lookup(sha) - root_tree = commit.tree + rugged_commit = repository.lookup(sha) + root_tree = rugged_commit.tree blob_entry = find_entry_by_path(repository, root_tree.oid, path) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 145721dea76..016437b2419 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -15,8 +15,6 @@ module Gitlab attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator - delegate :tree, to: :rugged_commit - def ==(other) return false unless other.is_a?(Gitlab::Git::Commit) @@ -452,6 +450,11 @@ module Gitlab ) end + # Is this the same as Blob.find_entry_by_path ? + def rugged_tree_entry(path) + rugged_commit.tree.path(path) + end + private def init_from_hash(hash) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d7605b74cf1..283134e043e 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1163,23 +1163,13 @@ module Gitlab end def fetch_repository_as_mirror(repository) - remote_name = "tmp-#{SecureRandom.hex}" - - # Notice that this feature flag is not for `fetch_repository_as_mirror` - # as a whole but for the fetching mechanism (file path or gitaly-ssh). - url, env = gitaly_migrate(:fetch_internal) do |is_enabled| + gitaly_migrate(:remote_fetch_internal_remote) do |is_enabled| if is_enabled - repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository) - [GITALY_INTERNAL_URL, repository.fetch_env] + gitaly_remote_client.fetch_internal_remote(repository) else - [repository.path, nil] + rugged_fetch_repository_as_mirror(repository) end end - - add_remote(remote_name, url, mirror_refmap: :all_refs) - fetch_remote(remote_name, env: env) - ensure - remove_remote(remote_name) end def blob_at(sha, path) @@ -2070,6 +2060,16 @@ module Gitlab false end + def rugged_fetch_repository_as_mirror(repository) + remote_name = "tmp-#{SecureRandom.hex}" + repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository) + + add_remote(remote_name, GITALY_INTERNAL_URL, mirror_refmap: :all_refs) + fetch_remote(remote_name, env: repository.fetch_env) + ensure + remove_remote(remote_name) + end + def fetch_remote(remote_name = 'origin', env: nil) run_git(['fetch', remote_name], env: env).last.zero? end diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb index 9218f6cfd68..559a901b9a3 100644 --- a/lib/gitlab/gitaly_client/remote_service.rb +++ b/lib/gitlab/gitaly_client/remote_service.rb @@ -23,6 +23,19 @@ module Gitlab response.result end + + def fetch_internal_remote(repository) + request = Gitaly::FetchInternalRemoteRequest.new( + repository: @gitaly_repo, + remote_repository: repository.gitaly_repository + ) + + response = GitalyClient.call(@storage, :remote_service, + :fetch_internal_remote, request, + remote_storage: repository.storage) + + response.result + end end end end @@ -10,6 +10,7 @@ module QA autoload :Namespace, 'qa/runtime/namespace' autoload :Scenario, 'qa/runtime/scenario' autoload :Browser, 'qa/runtime/browser' + autoload :Env, 'qa/runtime/env' end ## diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 220bb45741b..14b2a488760 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -38,22 +38,49 @@ module QA Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - 'chromeOptions' => { - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] + # This enables access to logs with `page.driver.manage.get_log(:browser)` + loggingPrefs: { + browser: "ALL", + client: "ALL", + driver: "ALL", + server: "ALL" } ) - Capybara::Selenium::Driver - .new(app, browser: :chrome, desired_capabilities: capabilities) - end + options = Selenium::WebDriver::Chrome::Options.new + options.add_argument("window-size=1240,1680") - Capybara::Screenshot.register_driver(:chrome) do |driver, path| - driver.browser.save_screenshot(path) + # Chrome won't work properly in a Docker container in sandbox mode + options.add_argument("no-sandbox") + + # Run headless by default unless CHROME_HEADLESS is false + if QA::Runtime::Env.chrome_headless? + options.add_argument("headless") + + # Chrome documentation says this flag is needed for now + # https://developers.google.com/web/updates/2017/04/headless-chrome#cli + options.add_argument("disable-gpu") + end + + # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 + options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci? + + Capybara::Selenium::Driver.new( + app, + browser: :chrome, + desired_capabilities: capabilities, + options: options + ) end # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run + # From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 + Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) + end + Capybara.configure do |config| config.default_driver = :chrome config.javascript_driver = :chrome diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb new file mode 100644 index 00000000000..d5c28e9a7db --- /dev/null +++ b/qa/qa/runtime/env.rb @@ -0,0 +1,15 @@ +module QA + module Runtime + module Env + extend self + + def chrome_headless? + (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0 + end + + def running_in_ci? + ENV['CI'] || ENV['CI_SERVER'] + end + end + end +end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb new file mode 100644 index 00000000000..57a72a04507 --- /dev/null +++ b/qa/spec/runtime/env_spec.rb @@ -0,0 +1,64 @@ +describe QA::Runtime::Env do + before do + allow(ENV).to receive(:[]).and_call_original + end + + describe '.chrome_headless?' do + context 'when there is an env variable set' do + it 'returns false when falsey values specified' do + stub_env('CHROME_HEADLESS', 'false') + expect(described_class.chrome_headless?).to be_falsey + + stub_env('CHROME_HEADLESS', 'no') + expect(described_class.chrome_headless?).to be_falsey + + stub_env('CHROME_HEADLESS', '0') + expect(described_class.chrome_headless?).to be_falsey + end + + it 'returns true when anything else specified' do + stub_env('CHROME_HEADLESS', 'true') + expect(described_class.chrome_headless?).to be_truthy + + stub_env('CHROME_HEADLESS', '1') + expect(described_class.chrome_headless?).to be_truthy + + stub_env('CHROME_HEADLESS', 'anything') + expect(described_class.chrome_headless?).to be_truthy + end + end + + context 'when there is no env variable set' do + it 'returns the default, true' do + stub_env('CHROME_HEADLESS', nil) + expect(described_class.chrome_headless?).to be_truthy + end + end + end + + describe '.running_in_ci?' do + context 'when there is an env variable set' do + it 'returns true if CI' do + stub_env('CI', 'anything') + expect(described_class.running_in_ci?).to be_truthy + end + + it 'returns true if CI_SERVER' do + stub_env('CI_SERVER', 'anything') + expect(described_class.running_in_ci?).to be_truthy + end + end + + context 'when there is no env variable set' do + it 'returns true' do + stub_env('CI', nil) + stub_env('CI_SERVER', nil) + expect(described_class.running_in_ci?).to be_falsey + end + end + end + + def stub_env(name, value) + allow(ENV).to receive(:[]).with(name).and_return(value) + end +end diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 305af289531..4d765229bde 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -55,6 +55,16 @@ describe Projects::BoardsController do end end + context 'issues are disabled' do + let(:project) { create(:project, :issues_disabled) } + + it 'returns a not found 404 response' do + list_boards + + expect(response).to have_gitlab_http_status(404) + end + end + def list_boards(format: :html) get :index, namespace_id: project.namespace, project_id: project, diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb index 435de3861cf..d820a59aa16 100644 --- a/spec/features/boards/keyboard_shortcut_spec.rb +++ b/spec/features/boards/keyboard_shortcut_spec.rb @@ -1,20 +1,38 @@ require 'rails_helper' describe 'Issue Boards shortcut', :js do - let(:project) { create(:project) } + context 'issues are enabled' do + let(:project) { create(:project) } - before do - create(:board, project: project) + before do + create(:board, project: project) - sign_in(create(:admin)) + sign_in(create(:admin)) - visit project_path(project) + visit project_path(project) + end + + it 'takes user to issue board index' do + find('body').native.send_keys('gb') + expect(page).to have_selector('.boards-list') + + wait_for_requests + end end - it 'takes user to issue board index' do - find('body').native.send_keys('gb') - expect(page).to have_selector('.boards-list') + context 'issues are not enabled' do + let(:project) { create(:project, :issues_disabled) } + + before do + sign_in(create(:admin)) + + visit project_path(project) + end + + it 'does not take user to the issue board index' do + find('body').native.send_keys('gb') - wait_for_requests + expect(page).to have_selector("body[data-page='projects:show']") + end end end diff --git a/spec/features/explore/groups_spec.rb b/spec/features/explore/groups_spec.rb new file mode 100644 index 00000000000..e4ef47d88dd --- /dev/null +++ b/spec/features/explore/groups_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe 'Explore Groups', :js do + let(:user) { create :user } + let(:group) { create :group } + let!(:private_project) do + create :project, :private, namespace: group do |project| + create(:issue, project: internal_project) + create(:merge_request, source_project: project, target_project: project) + end + end + + let!(:internal_project) do + create :project, :internal, namespace: group do |project| + create(:issue, project: project) + create(:merge_request, source_project: project, target_project: project) + end + end + + let!(:public_project) do + create(:project, :public, namespace: group) do |project| + create(:issue, project: project) + create(:merge_request, source_project: project, target_project: project) + end + end + + shared_examples 'renders public and internal projects' do + it do + visit_page + expect(page).to have_content(public_project.name) + expect(page).to have_content(internal_project.name) + expect(page).not_to have_content(private_project.name) + end + end + + shared_examples 'renders only public project' do + it do + visit_page + expect(page).to have_content(public_project.name) + expect(page).not_to have_content(internal_project.name) + expect(page).not_to have_content(private_project.name) + end + end + + shared_examples 'renders group in public groups area' do + it do + visit explore_groups_path + expect(page).to have_content(group.name) + end + end + + context 'when signed in' do + before do + sign_in(user) + end + + it_behaves_like 'renders public and internal projects' do + subject(:visit_page) { visit group_path(group) } + end + + it_behaves_like 'renders public and internal projects' do + subject(:visit_page) { visit issues_group_path(group) } + end + + it_behaves_like 'renders public and internal projects' do + subject(:visit_page) { visit merge_requests_group_path(group) } + end + + it_behaves_like 'renders group in public groups area' + end + + context 'when signed out' do + it_behaves_like 'renders only public project' do + subject(:visit_page) { visit group_path(group) } + end + + it_behaves_like 'renders only public project' do + subject(:visit_page) { visit issues_group_path(group) } + end + + it_behaves_like 'renders only public project' do + subject(:visit_page) { visit merge_requests_group_path(group) } + end + + it_behaves_like 'renders group in public groups area' + end +end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 7fc2b383749..ceccc471405 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -55,4 +55,20 @@ feature 'Group show page' do end end end + + context 'group has a project with emoji in description', :js do + let(:user) { create(:user) } + let!(:project) { create(:project, description: ':smile:', namespace: group) } + + before do + group.add_owner(user) + sign_in(user) + visit path + end + + it 'shows the project info' do + expect(page).to have_content(project.title) + expect(page).to have_selector('gl-emoji[data-name="smile"]') + end + end end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb new file mode 100644 index 00000000000..e4be6193b8b --- /dev/null +++ b/spec/features/invites_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe 'Invites' do + let(:user) { create(:user) } + let(:owner) { create(:user, name: 'John Doe') } + let(:group) { create(:group, name: 'Owned') } + let(:project) { create(:project, :repository, namespace: group) } + let(:invite) { group.group_members.invite.last } + + before do + project.add_master(owner) + group.add_user(owner, Gitlab::Access::OWNER) + group.add_developer('user@example.com', owner) + invite.generate_invite_token! + end + + context 'when signed out' do + before do + visit invite_path(invite.raw_invite_token) + end + + it 'renders sign in page with sign in notice' do + expect(current_path).to eq(new_user_session_path) + expect(page).to have_content('To accept this invitation, sign in') + end + + it 'sign in and redirects to invitation page' do + fill_in 'user_login', with: user.email + fill_in 'user_password', with: user.password + check 'user_remember_me' + click_button 'Sign in' + + expect(current_path).to eq(invite_path(invite.raw_invite_token)) + expect(page).to have_content( + 'You have been invited by John Doe to join group Owned as Developer.' + ) + expect(page).to have_link('Accept invitation') + expect(page).to have_link('Decline') + end + end + + context 'when signed in as an exists member' do + before do + sign_in(owner) + end + + it 'shows message user already a member' do + visit invite_path(invite.raw_invite_token) + expect(page).to have_content('However, you are already a member of this group.') + end + end + + describe 'accepting the invitation' do + before do + sign_in(user) + visit invite_path(invite.raw_invite_token) + end + + it 'grants access and redirects to group page' do + page.click_link 'Accept invitation' + expect(current_path).to eq(group_path(group)) + expect(page).to have_content( + 'You have been granted Developer access to group Owned.' + ) + end + end + + describe 'declining the application' do + context 'when signed in' do + before do + sign_in(user) + visit invite_path(invite.raw_invite_token) + end + + it 'declines application and redirects to dashboard' do + page.click_link 'Decline' + expect(current_path).to eq(dashboard_projects_path) + expect(page).to have_content( + 'You have declined the invitation to join group Owned.' + ) + end + end + + context 'when signed out' do + before do + visit decline_invite_path(invite.raw_invite_token) + end + + it 'declines application and redirects to sign in page' do + expect(current_path).to eq(new_user_session_path) + expect(page).to have_content( + 'You have declined the invitation to join group Owned.' + ) + end + end + end +end diff --git a/spec/features/issues/keyboard_shortcut_spec.rb b/spec/features/issues/keyboard_shortcut_spec.rb new file mode 100644 index 00000000000..961de9d3d25 --- /dev/null +++ b/spec/features/issues/keyboard_shortcut_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe 'Issues shortcut', :js do + context 'New Issue shortcut' do + context 'issues are enabled' do + let(:project) { create(:project) } + + before do + sign_in(create(:admin)) + + visit project_path(project) + end + + it 'takes user to the new issue page' do + find('body').native.send_keys('i') + expect(page).to have_selector('#new_issue') + end + end + + context 'issues are not enabled' do + let(:project) { create(:project, :issues_disabled) } + + before do + sign_in(create(:admin)) + + visit project_path(project) + end + + it 'does not take user to the new issue page' do + find('body').native.send_keys('i') + + expect(page).to have_selector("body[data-page='projects:show']") + end + end + end +end diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index bf6ada8185e..d07db871d69 100644 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -11,168 +11,38 @@ const createComponent = (propsData) => { }; describe('MonitoringDeployment', () => { - const reducedDeploymentData = [deploymentData[0]]; - reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name; - reducedDeploymentData[0].xPos = 10; - reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at); describe('Methods', () => { - it('refText shows the ref when a tag is available', () => { - reducedDeploymentData[0].tag = '1.0'; - const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect( - component.refText(reducedDeploymentData[0]), - ).toEqual(reducedDeploymentData[0].ref); - }); - - it('refText shows the sha when no tag is available', () => { - reducedDeploymentData[0].tag = null; - const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.refText(reducedDeploymentData[0]), - ).toContain('f5bcd1'); - }); - - it('nameDeploymentClass creates a class with the prefix deploy-info-', () => { + it('should contain a hidden gradient', () => { const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, + showDeployInfo: true, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, }); - expect( - component.nameDeploymentClass(reducedDeploymentData[0]), - ).toContain('deploy-info'); + expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); }); it('transformDeploymentGroup translates an available deployment', () => { const component = createComponent({ showDeployInfo: false, - deploymentData: reducedDeploymentData, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, }); expect( - component.transformDeploymentGroup(reducedDeploymentData[0]), + component.transformDeploymentGroup({ xPos: 16 }), ).toContain('translate(11, 20)'); }); - it('hides the deployment flag', () => { - reducedDeploymentData[0].showDeploymentFlag = false; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull(); - }); - - it('positions the flag to the left when the xPos is too far right', () => { - reducedDeploymentData[0].showDeploymentFlag = false; - reducedDeploymentData[0].xPos = 250; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect( - component.positionFlag(reducedDeploymentData[0]), - ).toBeLessThan(0); - }); - - it('shows the deployment flag', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelector('.js-deploy-info-box').style.display, - ).not.toEqual('display: none;'); - }); - - it('contains date, refs and the "deployed" text', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText('Deployed'); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText('Wed, May 31'); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText(component.refText(reducedDeploymentData[0])); - }); - - it('contains a link to the commit contents', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'), - ).not.toEqual(''); - }); - - it('should contain a hidden gradient', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); - }); - describe('Computed props', () => { it('calculatedHeight', () => { const component = createComponent({ showDeployInfo: true, - deploymentData: reducedDeploymentData, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 8ee1171419d..2d474e9092f 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue'; +import { deploymentData } from '../mock_data'; const createComponent = (propsData) => { const Component = Vue.extend(GraphFlag); @@ -9,11 +10,6 @@ const createComponent = (propsData) => { }).$mount(); }; -function getCoordinate(component, selector, coordinate) { - const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate); - return parseInt(coordinateVal, 10); -} - const defaultValuesComponent = { currentXCoordinate: 200, currentYCoordinate: 100, @@ -25,31 +21,111 @@ const defaultValuesComponent = { graphHeight: 300, graphHeightOffset: 120, showFlagContent: true, + realPixelRatio: 1, + timeSeries: [{ + values: [{ + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }], + }], + unitOfDisplay: 'ms', + currentDataIndex: 0, + legendTitle: 'Average', +}; + +const deploymentFlagData = { + ...deploymentData[0], + ref: deploymentData[0].ref.name, + xPos: 10, + time: new Date(deploymentData[0].created_at), }; describe('GraphFlag', () => { - it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { - const component = createComponent(defaultValuesComponent); + let component; - expect(getCoordinate(component, '.selected-metric-line', 'x1')) - .toEqual(component.currentXCoordinate); - expect(getCoordinate(component, '.selected-metric-line', 'x2')) - .toEqual(component.currentXCoordinate); + it('has a line at the currentXCoordinate', () => { + component = createComponent(defaultValuesComponent); + + expect(component.$el.style.left) + .toEqual(`${70 + component.currentXCoordinate}px`); }); - it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { - const component = createComponent(defaultValuesComponent); + describe('Deployment flag', () => { + it('shows a deployment flag when deployment data provided', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.popover-title'), + ).toContainText('Deployed'); + }); + + it('contains the ref when a tag is available', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData: { + ...deploymentFlagData, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + tag: true, + ref: '1.0', + }, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('f5bcd1d9'); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('1.0'); + }); + + it('does not contain the ref when a tag is unavailable', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData: { + ...deploymentFlagData, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + tag: false, + ref: '1.0', + }, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('f5bcd1d9'); - const svg = component.$el.querySelector('.rect-text-metric'); - expect(svg.tagName).toEqual('svg'); - expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition); + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).not.toContainText('1.0'); + }); }); describe('Computed props', () => { - it('calculatedHeight', () => { - const component = createComponent(defaultValuesComponent); + beforeEach(() => { + component = createComponent(defaultValuesComponent); + }); + + it('formatTime', () => { + expect(component.formatTime).toMatch(/\d:17PM/); + }); + + it('formatDate', () => { + expect(component.formatDate).toEqual('Sun, Jun 4'); + }); + + it('cursorStyle', () => { + expect(component.cursorStyle).toEqual({ + top: '20px', + left: '270px', + height: '180px', + }); + }); - expect(component.calculatedHeight).toEqual(180); + it('flagOrientation', () => { + expect(component.flagOrientation).toEqual('left'); }); }); }); diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 7727a1d81b1..43761c2fe0c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1006,12 +1006,12 @@ describe Gitlab::Database::MigrationHelpers do context 'with batch_size option' do it 'queues jobs correctly' do Sidekiq::Testing.fake! do - model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2) + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2) expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) - expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f) end end end @@ -1019,10 +1019,10 @@ describe Gitlab::Database::MigrationHelpers do context 'without batch_size option' do it 'queues jobs correctly' do Sidekiq::Testing.fake! do - model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds) + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes) expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f) end end end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index 7322a326b01..6193e177668 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -73,4 +73,19 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do described_class.new(key, timeout: 3600).try_obtain end end + + describe '#ttl' do + it 'returns the TTL of the Redis key' do + lease = described_class.new('kittens', timeout: 100) + lease.try_obtain + + expect(lease.ttl <= 100).to eq(true) + end + + it 'returns nil when the lease does not exist' do + lease = described_class.new('kittens', timeout: 10) + + expect(lease.ttl).to be_nil + end + end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 7f5946b1658..07eb5b82d5f 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -146,7 +146,7 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'when sha references a tree' do it 'returns nil' do - tree = Gitlab::Git::Commit.find(repository, 'master').tree + tree = repository.rugged.rev_parse('master^{tree}') blob = Gitlab::Git::Blob.raw(repository, tree.oid) @@ -230,7 +230,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end describe '.batch_lfs_pointers' do - let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree } + let(:tree_object) { repository.rugged.rev_parse('master^{tree}') } let(:non_lfs_blob) do Gitlab::Git::Blob.find( diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 6d35734d306..6a07a3ca8b8 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -55,7 +55,6 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(@commit.parents).to eq(@gitlab_parents) } it { expect(@commit.parent_id).to eq(@parents.first.oid) } it { expect(@commit.no_commit_message).to eq("--no commit message") } - it { expect(@commit.tree).to eq(@tree) } after do # Erase the new commit so other tests get the original repo diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f94234f6010..f346a345f00 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -649,29 +649,39 @@ describe Gitlab::Git::Repository, seed_helper: true do Gitlab::Shell.new.remove_repository(storage_path, 'my_project') end - it 'fetches a repository as a mirror remote' do - subject + shared_examples 'repository mirror fecthing' do + it 'fetches a repository as a mirror remote' do + subject - expect(refs(new_repository.path)).to eq(refs(repository.path)) - end + expect(refs(new_repository.path)).to eq(refs(repository.path)) + end - context 'with keep-around refs' do - let(:sha) { SeedRepo::Commit::ID } - let(:keep_around_ref) { "refs/keep-around/#{sha}" } - let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } + context 'with keep-around refs' do + let(:sha) { SeedRepo::Commit::ID } + let(:keep_around_ref) { "refs/keep-around/#{sha}" } + let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } - before do - repository.rugged.references.create(keep_around_ref, sha, force: true) - repository.rugged.references.create(tmp_ref, sha, force: true) - end + before do + repository.rugged.references.create(keep_around_ref, sha, force: true) + repository.rugged.references.create(tmp_ref, sha, force: true) + end - it 'includes the temporary and keep-around refs' do - subject + it 'includes the temporary and keep-around refs' do + subject - expect(refs(new_repository.path)).to include(keep_around_ref) - expect(refs(new_repository.path)).to include(tmp_ref) + expect(refs(new_repository.path)).to include(keep_around_ref) + expect(refs(new_repository.path)).to include(tmp_ref) + end end end + + context 'with gitaly enabled' do + it_behaves_like 'repository mirror fecthing' + end + + context 'with gitaly enabled', :skip_gitaly_mock do + it_behaves_like 'repository mirror fecthing' + end end describe '#remote_tags' do diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb index 69c6f054016..9d540446532 100644 --- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb @@ -31,4 +31,17 @@ describe Gitlab::GitalyClient::RemoteService do expect(client.remove_remote(remote_name)).to be(true) end end + + describe '#fetch_internal_remote' do + let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + it 'sends an fetch_internal_remote message and returns the result value' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:fetch_internal_remote) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(result: true)) + + expect(client.fetch_internal_remote(remote_repository)).to be(true) + end + end end diff --git a/spec/migrations/normalize_ldap_extern_uids_spec.rb b/spec/migrations/normalize_ldap_extern_uids_spec.rb index 262d7742aaf..56a78f52802 100644 --- a/spec/migrations/normalize_ldap_extern_uids_spec.rb +++ b/spec/migrations/normalize_ldap_extern_uids_spec.rb @@ -27,11 +27,11 @@ describe NormalizeLdapExternUids, :migration, :sidekiq do migrate! expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(5.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) - expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) - expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(30.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(15.minutes.from_now.to_f) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 4f02dc33cd8..817254c7d1e 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -181,7 +181,6 @@ eos it { is_expected.to respond_to(:parents) } it { is_expected.to respond_to(:date) } it { is_expected.to respond_to(:diffs) } - it { is_expected.to respond_to(:tree) } it { is_expected.to respond_to(:id) } it { is_expected.to respond_to(:to_patch) } end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 32f40f8c365..00afa09f1a3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3079,9 +3079,51 @@ describe Project do expect(project).to receive(:import_finish) expect(project).to receive(:update_project_counter_caches) expect(project).to receive(:remove_import_jid) + expect(project).to receive(:after_create_default_branch) project.after_import end + + context 'branch protection' do + let(:project) { create(:project, :repository) } + + it 'does not protect when branch protection is disabled' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) + + project.after_import + + expect(project.protected_branches).to be_empty + end + + it "gives developer access to push when branch protection is set to 'developers can push'" do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + project.after_import + + expect(project.protected_branches).not_to be_empty + expect(project.default_branch).to eq(project.protected_branches.first.name) + expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) + end + + it "gives developer access to merge when branch protection is set to 'developers can merge'" do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + project.after_import + + expect(project.protected_branches).not_to be_empty + expect(project.default_branch).to eq(project.protected_branches.first.name) + expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) + end + + it 'protects default branch' do + project.after_import + + expect(project.protected_branches).not_to be_empty + expect(project.default_branch).to eq(project.protected_branches.first.name) + expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + end + end end describe '#update_project_counter_caches' do diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 452754d7a79..505a9eaac5a 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -22,6 +22,7 @@ describe GroupChildEntity do avatar_url name description + markdown_description visibility type can_edit @@ -60,9 +61,10 @@ describe GroupChildEntity do end describe 'for a group', :nested_groups do + let(:description) { 'Awesomeness' } let(:object) do create(:group, :nested, :with_avatar, - description: 'Awesomeness') + description: description) end before do @@ -96,6 +98,14 @@ describe GroupChildEntity do expect(json[:edit_path]).to eq(edit_group_path(object)) end + context 'emoji in description' do + let(:description) { ':smile:' } + + it 'has the correct markdown_description' do + expect(json[:markdown_description]).to eq('<p dir="auto"><gl-emoji title="smiling face with open mouth and smiling eyes" data-name="smile" data-unicode-version="6.0">😄</gl-emoji></p>') + end + end + it_behaves_like 'group child json' end end diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 835e83d6dba..53b3e5e365d 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -19,5 +19,21 @@ describe ProtectedBranches::CreateService do expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) end + + context 'when user does not have permission' do + let(:user) { create(:user) } + + before do + project.add_developer(user) + end + + it 'creates a new protected branch if we skip authorization step' do + expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1) + end + + it 'raises Gitlab::Access:AccessDeniedError' do + expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end end end diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb index 1c54cf55fa0..d67e7698635 100644 --- a/spec/workers/background_migration_worker_spec.rb +++ b/spec/workers/background_migration_worker_spec.rb @@ -1,13 +1,32 @@ require 'spec_helper' -describe BackgroundMigrationWorker, :sidekiq do +describe BackgroundMigrationWorker, :sidekiq, :clean_gitlab_redis_shared_state do + let(:worker) { described_class.new } + describe '.perform' do it 'performs a background migration' do expect(Gitlab::BackgroundMigration) .to receive(:perform) .with('Foo', [10, 20]) - described_class.new.perform('Foo', [10, 20]) + worker.perform('Foo', [10, 20]) + end + + it 'reschedules a migration if it was performed recently' do + expect(worker) + .to receive(:always_perform?) + .and_return(false) + + worker.lease_for('Foo').try_obtain + + expect(Gitlab::BackgroundMigration) + .not_to receive(:perform) + + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20]) + + worker.perform('Foo', [10, 20]) end end end diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 85ac14eb347..7274a9f00f9 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -32,6 +32,7 @@ describe RepositoryImportWorker do expect_any_instance_of(Projects::ImportService).to receive(:execute) .and_return({ status: :ok }) + expect_any_instance_of(Project).to receive(:after_import).and_call_original expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) expect_any_instance_of(Project).to receive(:import_finish) |