Commit adc84db2 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-01-25

# Conflicts:
#	app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
#	app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
#	app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
#	app/assets/javascripts/vue_merge_request_widget/dependencies.js

[ci skip]
parents 7539fbed 72e5a499
...@@ -53,10 +53,10 @@ ...@@ -53,10 +53,10 @@
</i> </i>
</div> </div>
<div class="deploy-key-content key-list-item-info"> <div class="deploy-key-content key-list-item-info">
<strong class="title"> <strong class="title qa-key-title">
{{ deployKey.title }} {{ deployKey.title }}
</strong> </strong>
<div class="description"> <div class="description qa-key-fingerprint">
{{ deployKey.fingerprint }} {{ deployKey.fingerprint }}
</div> </div>
</div> </div>
......
...@@ -2,7 +2,7 @@ import { getTimeago } from '~/lib/utils/datetime_utility'; ...@@ -2,7 +2,7 @@ import { getTimeago } from '~/lib/utils/datetime_utility';
import { visitUrl } from '../../lib/utils/url_utility'; import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage'; import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon'; import StatusIcon from './mr_widget_status_icon.vue';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../services/mr_widget_service';
export default { export default {
......
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
status: { type: String, required: true },
showDisabledButton: { type: Boolean, required: false },
},
components: {
ciIcon,
loadingIcon,
},
computed: {
statusObj() {
return {
group: this.status,
icon: `status_${this.status}`,
};
},
},
template: `
<div class="space-children flex-container-block append-right-10">
<div v-if="status === 'loading'" class="mr-widget-icon">
<loading-icon />
</div>
<ci-icon v-else :status="statusObj" />
<button
v-if="showDisabledButton"
type="button"
class="js-disabled-merge-button btn btn-success btn-sm"
disabled="true">
Merge
</button>
</div>
`,
};
<script>
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
ciIcon,
loadingIcon,
},
props: {
status: {
type: String,
required: true,
},
showDisabledButton: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isLoading() {
return this.status === 'loading';
},
statusObj() {
return {
group: this.status,
icon: `status_${this.status}`,
};
},
},
};
</script>
<template>
<div class="space-children flex-container-block append-right-10">
<div
v-if="isLoading"
class="mr-widget-icon"
>
<loading-icon />
</div>
<ci-icon
v-else
:status="statusObj"
/>
<button
v-if="showDisabledButton"
type="button"
class="js-disabled-merge-button btn btn-success btn-sm"
disabled="true"
>
{{ s__("mrWidget|Merge") }}
</button>
</div>
</template>
<script> <script>
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetArchived', name: 'MRWidgetArchived',
......
<script> <script>
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetAutoMergeFailed', name: 'MRWidgetAutoMergeFailed',
......
<script> <script>
<<<<<<< HEAD
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon';
=======
import statusIcon from '../mr_widget_status_icon.vue';
>>>>>>> upstream/master
export default { export default {
name: 'MRWidgetChecking', name: 'MRWidgetChecking',
......
<script> <script>
import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
<<<<<<< HEAD
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon';
=======
import statusIcon from '../mr_widget_status_icon.vue';
>>>>>>> upstream/master
export default { export default {
name: 'MRWidgetClosed', name: 'MRWidgetClosed',
......
<script> <script>
<<<<<<< HEAD
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon';
=======
import statusIcon from '../mr_widget_status_icon.vue';
>>>>>>> upstream/master
export default { export default {
name: 'MRWidgetConflicts', name: 'MRWidgetConflicts',
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
......
import Flash from '../../../flash'; import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import MRWidgetAuthor from '../../components/mr_widget_author'; import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
......
...@@ -2,7 +2,7 @@ import Flash from '../../../flash'; ...@@ -2,7 +2,7 @@ import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
......
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetMerging',
props: {
mr: { type: Object, required: true },
},
components: {
statusIcon,
},
template: `
<div class="mr-widget-body mr-state-locked media">
<status-icon status="loading" />
<div class="media-body">
<h4>
This merge request is in the process of being merged
</h4>
<section class="mr-info-list">
<p>
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</p>
</section>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetMerging',
components: {
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
},
};
</script>
<template>
<div class="mr-widget-body mr-state-locked media">
<status-icon status="loading" />
<div class="media-body">
<h4>
{{ s__("mrWidget|This merge request is in the process of being merged") }}
</h4>
<section class="mr-info-list">
<p>
{{ s__("mrWidget|The changes will be merged into") }}
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
</p>
</section>
</div>
</div>
</template>
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help'; import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetNotAllowed', name: 'MRWidgetNotAllowed',
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetPipelineBlocked', name: 'MRWidgetPipelineBlocked',
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetPipelineBlocked', name: 'MRWidgetPipelineBlocked',
......
...@@ -3,7 +3,7 @@ import warningSvg from 'icons/_icon_status_warning.svg'; ...@@ -3,7 +3,7 @@ import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
import MergeRequest from '../../../merge_request'; import MergeRequest from '../../../merge_request';
import Flash from '../../../flash'; import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
......
<script> <script>
import simplePoll from '../../../lib/utils/simple_poll'; import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetSHAMismatch', name: 'MRWidgetSHAMismatch',
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetUnresolvedDiscussions', name: 'MRWidgetUnresolvedDiscussions',
......
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
......
...@@ -19,7 +19,11 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li ...@@ -19,7 +19,11 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li
export { default as MergedState } from './components/states/mr_widget_merged'; export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
<<<<<<< HEAD
export { default as MergingState } from './components/states/mr_widget_merging'; export { default as MergingState } from './components/states/mr_widget_merging';
=======
export { default as MergingState } from './components/states/mr_widget_merging.vue';
>>>>>>> upstream/master
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
...@@ -35,8 +39,13 @@ export { default as MergeWhenPipelineSucceedsState } from './components/states/m ...@@ -35,8 +39,13 @@ export { default as MergeWhenPipelineSucceedsState } from './components/states/m
export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
export { default as CheckingState } from './components/states/mr_widget_checking.vue'; export { default as CheckingState } from './components/states/mr_widget_checking.vue';
<<<<<<< HEAD
export { default as MRWidgetStore } from 'ee/vue_merge_request_widget/stores/mr_widget_store'; export { default as MRWidgetStore } from 'ee/vue_merge_request_widget/stores/mr_widget_store';
export { default as MRWidgetService } from 'ee/vue_merge_request_widget/services/mr_widget_service'; export { default as MRWidgetService } from 'ee/vue_merge_request_widget/services/mr_widget_service';
=======
export { default as MRWidgetStore } from './stores/mr_widget_store';
export { default as MRWidgetService } from './services/mr_widget_service';
>>>>>>> upstream/master
export { default as eventHub } from './event_hub'; export { default as eventHub } from './event_hub';
export { default as getStateKey } from 'ee/vue_merge_request_widget/stores/get_state_key'; export { default as getStateKey } from 'ee/vue_merge_request_widget/stores/get_state_key';
export { default as mrWidgetOptions } from 'ee/vue_merge_request_widget/mr_widget_options'; export { default as mrWidgetOptions } from 'ee/vue_merge_request_widget/mr_widget_options';
......
...@@ -314,8 +314,8 @@ ...@@ -314,8 +314,8 @@
} }
&.invalid { &.invalid {
@include status-color($gray-dark, $gray, $common-gray-dark); @include status-color($gray-dark, $gray, $gray-darkest);
border-color: $common-gray-light; border-color: $gray-darkest;
} }
} }
...@@ -339,8 +339,8 @@ ...@@ -339,8 +339,8 @@
&.invalid { &.invalid {
svg { svg {
border: 1px solid $common-gray-light; border: 1px solid $gray-darkest;
fill: $common-gray-light; fill: $gray-darkest;
} }
} }
......
...@@ -8,7 +8,8 @@ class HealthController < ActionController::Base ...@@ -8,7 +8,8 @@ class HealthController < ActionController::Base
Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck, Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck, Gitlab::HealthChecks::Redis::SharedStateCheck,
Gitlab::HealthChecks::FsShardsCheck Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze ].freeze
def readiness def readiness
......
...@@ -14,13 +14,13 @@ module AutoDevopsHelper ...@@ -14,13 +14,13 @@ module AutoDevopsHelper
if missing_service if missing_service
params = { params = {
kubernetes: link_to('Kubernetes service', edit_project_service_path(project, 'kubernetes')) kubernetes: link_to('Kubernetes cluster', project_clusters_path(project))
} }
if missing_domain if missing_domain
_('Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly.') % params _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params
else else
_('Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly.') % params _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params
end end
elsif missing_domain elsif missing_domain
_('Auto Review Apps and Auto Deploy need a domain name to work correctly.') _('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
......
...@@ -1016,8 +1016,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -1016,8 +1016,14 @@ class MergeRequest < ActiveRecord::Base
merged_at = metrics&.merged_at merged_at = metrics&.merged_at
notes_association = notes_with_associations notes_association = notes_with_associations
# It is not guaranteed that Note#created_at will be strictly later than
# MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
# comparison, as will a HA environment if clocks are not *precisely*
# synchronized. Add a minute's leeway to compensate for both possibilities
cutoff = merged_at - 1.minute
if merged_at if merged_at
notes_association = notes_association.where('created_at > ?', merged_at) notes_association = notes_association.where('created_at >= ?', cutoff)
end end
!merge_commit.has_been_reverted?(current_user, notes_association) !merge_commit.has_been_reverted?(current_user, notes_association)
......
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
- if @user.avatar? - if @user.avatar?
You can change your avatar here You can change your avatar here
- if gravatar_enabled? - if gravatar_enabled?
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
- else - else
You can upload an avatar here You can upload an avatar here
- if gravatar_enabled? - if gravatar_enabled?
or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
.col-lg-8 .col-lg-8
.clearfix.avatar-image.append-bottom-default .clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
......
- title = capture do - title = capture do
This commit was signed with a different user's verified signature. This commit was signed with a different user's verified signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true } - locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
This commit was signed with a verified signature, but the committer email This commit was signed with a verified signature, but the committer email
is <strong>not verified</strong> to belong to the same user. is <strong>not verified</strong> to belong to the same user.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true } - locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- title = capture do - title = capture do
.gpg-popover-status .gpg-popover-status
.gpg-popover-icon{ class: css_class } .gpg-popover-icon{ class: css_class }
= render "shared/icons/#{icon}.svg" = sprite_icon(icon)
%div %div
= title = title
......
- title = capture do - title = capture do
This commit was signed with an <strong>unverified</strong> signature. This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' } - locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
This commit was signed with a <strong>verified</strong> signature and the This commit was signed with a <strong>verified</strong> signature and the
committer email is verified to belong to the same user. committer email is verified to belong to the same user.
- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true } - locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg>
---
title: Link Auto DevOps settings to Clusters page
merge_request: 16641
author:
type: changed
---
title: Fix encoding issue when counting commit count
merge_request: 16637
author:
type: fixed
---
title: Replace verified badge icons and uniform colors
merge_request:
author:
type: fixed
---
title: Default to HTTPS for all Gravatar URLs
merge_request: 16666
author:
type: fixed
---
title: Make Gitaly RepositoryExists opt-out
merge_request: 16680
author:
type: other
---
title: Add a gRPC health check to ensure Gitaly is up
merge_request:
author:
type: added
---
title: Add note within ux documentation that further changes should be made within
the design.gitlab project
merge_request:
author:
type: deprecated
...@@ -197,10 +197,12 @@ production: &base ...@@ -197,10 +197,12 @@ production: &base
host: 'https://mattermost.example.com' host: 'https://mattermost.example.com'
## Gravatar ## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html ## If using gravatar.com, there's nothing to change here. For Libravatar
## you'll need to provide the custom URLs. For more information,
## see: https://docs.gitlab.com/ee/customization/libravatar.html
gravatar: gravatar:
# gravatar urls: possible placeholders: %{hash} %{size} %{email} %{username} # Gravatar/Libravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
# plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # plain_url: "http://..." # default: https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
## Auxiliary jobs ## Auxiliary jobs
......
...@@ -410,7 +410,7 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled ...@@ -410,7 +410,7 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
# #
Settings['gravatar'] ||= Settingslogic.new({}) Settings['gravatar'] ||= Settingslogic.new({})
Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil? Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil?
Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['plain_url'] ||= 'https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
Settings.gravatar['host'] = Settings.host_without_www(Settings.gravatar['plain_url']) Settings.gravatar['host'] = Settings.host_without_www(Settings.gravatar['plain_url'])
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
# repository-wide language statistics: # repository-wide language statistics:
# <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36> # <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36>
# #
# The options passed by Linguist are those assumed by Gitlab::Git::Attributes # The options passed by Linguist are those assumed by Gitlab::Git::InfoAttributes
# anyway, and there is no great efficiency gain from just fetching the listed # anyway, and there is no great efficiency gain from just fetching the listed
# attributes with our implementation, so we ignore the additional arguments. # attributes with our implementation, so we ignore the additional arguments.
# #
...@@ -19,7 +19,7 @@ module Rugged ...@@ -19,7 +19,7 @@ module Rugged
end end
def attributes def attributes
@attributes ||= Gitlab::Git::Attributes.new(path) @attributes ||= Gitlab::Git::InfoAttributes.new(path)
end end
end end
......
> We are in the process of transferring UX documentation to the [design.gitlab.com](https://gitlab.com/gitlab-org/design.gitlab.com) project. Any updates to these docs should be made in that project. If documentation does not yet exist within [design.gitlab.com](https://gitlab.com/gitlab-org/design.gitlab.com), [create an issue](https://gitlab.com/gitlab-org/design.gitlab.com/issues) and merge request to add your new changes.
# GitLab UX Guide # GitLab UX Guide
The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions. The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions.
......
...@@ -66,9 +66,8 @@ To make full use of Auto DevOps, you will need: ...@@ -66,9 +66,8 @@ To make full use of Auto DevOps, you will need:
a domain configured with wildcard DNS which is gonna be used by all of your a domain configured with wildcard DNS which is gonna be used by all of your
Auto DevOps applications. [Read the specifics](#auto-devops-base-domain). Auto DevOps applications. [Read the specifics](#auto-devops-base-domain).
1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) - 1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) -
To enable deployments, you will need Kubernetes 1.5+. The [Kubernetes service][kubernetes-service] To enable deployments, you will need Kubernetes 1.5+. You need a [Kubernetes cluster][kubernetes-clusters]
integration will need to be enabled for the project, or enabled as a for the project, or a Kubernetes [default service template](../../user/project/integrations/services_templates.md)
[default service template](../../user/project/integrations/services_templates.md)
for the entire GitLab installation. for the entire GitLab installation.
1. **A load balancer** - You can use NGINX ingress by deploying it to your 1. **A load balancer** - You can use NGINX ingress by deploying it to your
Kubernetes cluster using the Kubernetes cluster using the
...@@ -588,7 +587,7 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ ...@@ -588,7 +587,7 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
``` ```
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
[kubernetes-service]: ../../user/project/integrations/kubernetes.md [kubernetes-clusters]: ../../user/project/clusters/index.md
[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor [docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../../ci/review_apps/index.md [review-app]: ../../ci/review_apps/index.md
[container-registry]: ../../user/project/container_registry.md [container-registry]: ../../user/project/container_registry.md
......
module Gitlab
module Git
# Parses root .gitattributes file at a given ref
class AttributesAtRefParser
delegate :attributes, to: :@parser
def initialize(repository, ref)
blob = repository.blob_at(ref, '.gitattributes')
@parser = AttributesParser.new(blob&.data)
end
end
end
end
# Gitaly note: JV: not sure what to make of this class. Why does it use
# the full disk path of the repository to look up attributes This is
# problematic in Gitaly, because Gitaly hides the full disk path to the
# repository from gitlab-ce.
module Gitlab module Gitlab
module Git module Git
# Class for parsing Git attribute files and extracting the attributes for # Class for parsing Git attribute files and extracting the attributes for
# file patterns. # file patterns.
# class AttributesParser
# Unlike Rugged this parser only needs a single IO call (a call to `open`), def initialize(attributes_data)
# vastly reducing the time spent in extracting attributes. @data = attributes_data || ""
#
# This class _only_ supports parsing the attributes file located at if @data.is_a?(File)
# `$GIT_DIR/info/attributes` as GitLab doesn't use any other files @patterns = parse_file
# (`.gitattributes` is copied to this particular path). end
#
# Basic usage:
#
# attributes = Gitlab::Git::Attributes.new(some_repo.path)
#
# attributes.attributes('README.md') # => { "eol" => "lf }
class Attributes
# path - The path to the Git repository.
def initialize(path)
@path = File.expand_path(path)
@patterns = nil
end end
# Returns all the Git attributes for the given path. # Returns all the Git attributes for the given path.
# #
# path - A path to a file for which to get the attributes. # file_path - A path to a file for which to get the attributes.
# #
# Returns a Hash. # Returns a Hash.
def attributes(path) def attributes(file_path)
full_path = File.join(@path, path) absolute_path = File.join('/', file_path)
patterns.each do |pattern, attrs| patterns.each do |pattern, attrs|
return attrs if File.fnmatch?(pattern, full_path) return attrs if File.fnmatch?(pattern, absolute_path)
end end
{} {}
...@@ -98,16 +82,10 @@ module Gitlab ...@@ -98,16 +82,10 @@ module Gitlab
# Iterates over every line in the attributes file. # Iterates over every line in the attributes file.
def each_line def each_line
full_path = File.join(@path, 'info/attributes') @data.each_line do |line|
break unless line.valid_encoding?
return unless File.exist?(full_path) yield line.strip
File.open(full_path, 'r') do |handle|
handle.each_line do |line|
break unless line.valid_encoding?
yield line.strip
end
end end
end end
...@@ -125,7 +103,8 @@ module Gitlab ...@@ -125,7 +103,8 @@ module Gitlab
parsed = attrs ? parse_attributes(attrs) : {} parsed = attrs ? parse_attributes(attrs) : {}
pairs << [File.join(@path, pattern), parsed] absolute_pattern = File.join('/', pattern)
pairs << [absolute_pattern, parsed]
end end
# Newer entries take precedence over older entries. # Newer entries take precedence over older entries.
......
# Gitaly note: JV: not sure what to make of this class. Why does it use
# the full disk path of the repository to look up attributes This is
# problematic in Gitaly, because Gitaly hides the full disk path to the
# repository from gitlab-ce.
module Gitlab
module Git
# Parses gitattributes at `$GIT_DIR/info/attributes`
#
# Unlike Rugged this parser only needs a single IO call (a call to `open`),
# vastly reducing the time spent in extracting attributes.
#
# This class _only_ supports parsing the attributes file located at
# `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
# (`.gitattributes` is copied to this particular path).
#
# Basic usage:
#
# attributes = Gitlab::Git::InfoAttributes.new(some_repo.path)
#
# attributes.attributes('README.md') # => { "eol" => "lf }
class InfoAttributes
delegate :attributes, :patterns, to: :parser
# path - The path to the Git repository.
def initialize(path)
@repo_path = File.expand_path(path)
end
def parser
@parser ||= begin
if File.exist?(attributes_path)
File.open(attributes_path, 'r') do |file_handle|
AttributesParser.new(file_handle)
end
else
AttributesParser.new("")
end
end
end
private
def attributes_path
@attributes_path ||= File.join(@repo_path, 'info/attributes')
end
end
end
end
...@@ -102,7 +102,7 @@ module Gitlab ...@@ -102,7 +102,7 @@ module Gitlab
) )
@path = File.join(storage_path, @relative_path) @path = File.join(storage_path, @relative_path)
@name = @relative_path.split("/").last @name = @relative_path.split("/").last
@attributes = Gitlab::Git::Attributes.new(path) @attributes = Gitlab::Git::InfoAttributes.new(path)
end end
def ==(other) def ==(other)
...@@ -133,7 +133,7 @@ module Gitlab ...@@ -133,7 +133,7 @@ module Gitlab
end end
def exists? def exists?
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_repository_client.exists? gitaly_repository_client.exists?
else else
...@@ -490,7 +490,11 @@ module Gitlab ...@@ -490,7 +490,11 @@ module Gitlab
return [] return []
end end
log_by_shell(sha, options) if log_using_shell?(options)
log_by_shell(sha, options)
else
log_by_walk(sha, options)
end
end end
def count_commits(options) def count_commits(options)
...@@ -991,6 +995,18 @@ module Gitlab ...@@ -991,6 +995,18 @@ module Gitlab
attributes(path)[name] attributes(path)[name]
end end
# Check .gitattributes for a given ref
#
# This only checks the root .gitattributes file,
# it does not traverse subfolders to find additional .gitattributes files
#
# This method is around 30 times slower than `attributes`,
# which uses `$GIT_DIR/info/attributes`
def attributes_at(ref, file_path)
parser = AttributesAtRefParser.new(self, ref)
parser.attributes(file_path)
end
def languages(ref = nil) def languages(ref = nil)
Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled| Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled|
if is_enabled if is_enabled
...@@ -1517,6 +1533,27 @@ module Gitlab ...@@ -1517,6 +1533,27 @@ module Gitlab
end end
end end
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
options[:skip_merges] ||
options[:after] ||
options[:before]
end
def log_by_walk(sha, options)
walk_options = {
show: sha,
sort: Rugged::SORT_NONE,
limit: options[:limit],
offset: options[:offset]
}
Rugged::Walker.walk(rugged, walk_options).to_a
end
# Gitaly note: JV: although #log_by_shell shells out to Git I think the
# complexity is such that we should migrate it as Ruby before trying to
# do it in Go.
def log_by_shell(sha, options) def log_by_shell(sha, options)
limit = options[:limit].to_i limit = options[:limit].to_i
offset = options[:offset].to_i offset = options[:offset].to_i
......
require 'base64' require 'base64'
require 'gitaly' require 'gitaly'
require 'grpc/health/v1/health_pb'
require 'grpc/health/v1/health_services_pb'
module Gitlab module Gitlab
module GitalyClient module GitalyClient
...@@ -69,14 +71,27 @@ module Gitlab ...@@ -69,14 +71,27 @@ module Gitlab
@stubs ||= {} @stubs ||= {}
@stubs[storage] ||= {} @stubs[storage] ||= {}
@stubs[storage][name] ||= begin @stubs[storage][name] ||= begin
klass = Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub) klass = stub_class(name)
addr = address(storage) addr = stub_address(storage)
addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp'
klass.new(addr, :this_channel_is_insecure) klass.new(addr, :this_channel_is_insecure)
end end
end end
end end
def self.stub_class(name)
if name == :health_check
Grpc::Health::V1::Health::Stub
else
Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub)
end
end
def self.stub_address(storage)
addr = address(storage)
addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp'
addr
end
def self.clear_stubs! def self.clear_stubs!
MUTEX.synchronize do MUTEX.synchronize do
@stubs = nil @stubs = nil
......
...@@ -125,11 +125,11 @@ module Gitlab ...@@ -125,11 +125,11 @@ module Gitlab
def commit_count(ref, options = {}) def commit_count(ref, options = {})
request = Gitaly::CountCommitsRequest.new( request = Gitaly::CountCommitsRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
revision: ref revision: encode_binary(ref)
) )
request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present? request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present?
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present? request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
request.path = options[:path] if options[:path].present? request.path = encode_binary(options[:path]) if options[:path].present?
request.max_count = options[:max_count] if options[:max_count].present? request.max_count = options[:max_count] if options[:max_count].present?
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
......
module Gitlab
module GitalyClient
class HealthCheckService
def initialize(storage)
@storage = storage
end
# Sends a gRPC health ping to the Gitaly server for the storage shard.
def check
request = Grpc::Health::V1::HealthCheckRequest.new
response = GitalyClient.call(@storage, :health_check, :check, request, timeout: GitalyClient.fast_timeout)
{ success: response&.status == :SERVING }
rescue GRPC::BadStatus => e
{ success: false, message: e.to_s }
end
end
end
end
module Gitlab
module HealthChecks
class GitalyCheck
extend BaseAbstractCheck
METRIC_PREFIX = 'gitaly_health_check'.freeze
class << self
def readiness
repository_storages.map do |storage_name|
check(storage_name)
end
end
def metrics
repository_storages.flat_map do |storage_name|
result, elapsed = with_timing { check(storage_name) }
labels = { shard: storage_name }
[
metric("#{metric_prefix}_success", successful?(result) ? 1 : 0, **labels),
metric("#{metric_prefix}_latency_seconds", elapsed, **labels)
].flatten
end
end
def check(storage_name)
serv = Gitlab::GitalyClient::HealthCheckService.new(storage_name)
result = serv.check
HealthChecks::Result.new(result[:success], result[:message], shard: storage_name)
end
private
def metric_prefix
METRIC_PREFIX
end
def successful?(result)
result[:success]
end
def repository_storages
storages.keys
end
def storages
Gitlab.config.repositories.storages
end
end
end
end
end
...@@ -195,13 +195,13 @@ msgstr "" ...@@ -195,13 +195,13 @@ msgstr ""
msgid "Author" msgid "Author"
msgstr "" msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly." msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly."
msgstr "" msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly." msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr "" msgstr ""
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly." msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr "" msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)" msgid "AutoDevOps|Auto DevOps (Beta)"
......
...@@ -6,4 +6,5 @@ gem 'capybara-screenshot', '~> 1.0.18' ...@@ -6,4 +6,5 @@ gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0' gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7' gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.8.0' gem 'selenium-webdriver', '~> 3.8.0'
gem 'net-ssh', require: false
gem 'airborne', '~> 0.2.13' gem 'airborne', '~> 0.2.13'
...@@ -46,6 +46,7 @@ GEM ...@@ -46,6 +46,7 @@ GEM
mini_mime (1.0.0) mini_mime (1.0.0)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.11.1) minitest (5.11.1)
net-ssh (4.1.0)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.8.1) nokogiri (1.8.1)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
...@@ -97,6 +98,7 @@ DEPENDENCIES ...@@ -97,6 +98,7 @@ DEPENDENCIES
airborne (~> 0.2.13) airborne (~> 0.2.13)
capybara (~> 2.16.1) capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18) capybara-screenshot (~> 1.0.18)
net-ssh
pry-byebug (~> 3.5.1) pry-byebug (~> 3.5.1)
rake (~> 12.3.0) rake (~> 12.3.0)
rspec (~> 3.7) rspec (~> 3.7)
......
...@@ -11,6 +11,7 @@ module QA ...@@ -11,6 +11,7 @@ module QA
autoload :Scenario, 'qa/runtime/scenario' autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser' autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env' autoload :Env, 'qa/runtime/env'
autoload :RSAKey, 'qa/runtime/rsa_key'
autoload :Address, 'qa/runtime/address' autoload :Address, 'qa/runtime/address'
autoload :API, 'qa/runtime/api' autoload :API, 'qa/runtime/api'
end end
......
...@@ -10,6 +10,12 @@ module QA ...@@ -10,6 +10,12 @@ module QA
end end
end end
product :fingerprint do
Page::Project::Settings::Repository.act do
expand_deploy_keys(&:key_fingerprint)
end
end
dependency Factory::Resource::Project, as: :project do |project| dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy' project.name = 'project-to-deploy'
project.description = 'project for adding deploy key test' project.description = 'project for adding deploy key test'
......
...@@ -41,7 +41,21 @@ module QA ...@@ -41,7 +41,21 @@ module QA
end end
def click_element(name) def click_element(name)
find(Page::Element.new(name).selector_css).click find_element(name).click
end
def find_element(name)
find(element_selector_css(name))
end
def within_element(name)
page.within(element_selector_css(name)) do
yield
end
end
def element_selector_css(name)
Page::Element.new(name).selector_css
end end
def self.path def self.path
......
...@@ -14,8 +14,8 @@ module QA ...@@ -14,8 +14,8 @@ module QA
end end
view 'app/assets/javascripts/deploy_keys/components/key.vue' do view 'app/assets/javascripts/deploy_keys/components/key.vue' do
element :key_title, /class=".*title.*"/ element :key_title, /class=".*qa-key-title.*"/
element :key_title_field, '{{ deployKey.title }}' element :key_fingerprint, /class=".*qa-key-fingerprint.*"/
end end
def fill_key_title(title) def fill_key_title(title)
...@@ -31,8 +31,22 @@ module QA ...@@ -31,8 +31,22 @@ module QA
end end
def key_title def key_title
page.within('.qa-project-deploy-keys') do within_project_deploy_keys do
page.find('.title').text find_element(:key_title).text
end
end
def key_fingerprint
within_project_deploy_keys do
find_element(:key_fingerprint).text
end
end
private
def within_project_deploy_keys
within_element(:project_deploy_keys) do
yield
end end
end end
end end
......
require 'net/ssh'
require 'forwardable'
module QA
module Runtime
class RSAKey
extend Forwardable
attr_reader :key
def_delegators :@key, :fingerprint
def initialize(bits = 4096)
@key = OpenSSL::PKey::RSA.new(bits)
end
def public_key
@public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}"
end
end
end
end
...@@ -10,17 +10,6 @@ module QA ...@@ -10,17 +10,6 @@ module QA
def password def password
ENV['GITLAB_PASSWORD'] || '5iveL!fe' ENV['GITLAB_PASSWORD'] || '5iveL!fe'
end end
def ssh_key
<<~KEY.delete("\n")
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
KEY
end
end end
end end
end end
module QA module QA
feature 'deploy keys support', :core do feature 'deploy keys support', :core do
given(:deploy_key_title) { 'deploy key title' }
given(:deploy_key_value) { Runtime::User.ssh_key }
scenario 'user adds a deploy key' do scenario 'user adds a deploy key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
key = Runtime::RSAKey.new
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
resource.title = deploy_key_title resource.title = deploy_key_title
resource.key = deploy_key_value resource.key = deploy_key_value
end end
expect(deploy_key.title).to eq(deploy_key_title) expect(deploy_key.title).to eq(deploy_key_title)
expect(deploy_key.fingerprint).to eq(key.fingerprint)
end end
end end
end end
describe QA::Runtime::RSAKey do
describe '#public_key' do
subject { described_class.new.public_key }
it 'generates a public RSA key' do
expect(subject).to match(/\Assh\-rsa AAAA[0-9A-Za-z+\/]+={0,3}\z/)
end
end
end
...@@ -91,7 +91,7 @@ x #ccc solid;padding-left:1ex"><div> ...@@ -91,7 +91,7 @@ x #ccc solid;padding-left:1ex"><div>
adding=3D"0" border=3D"0"><tbody> adding=3D"0" border=3D"0"><tbody>
<tr> <tr>
<td style=3D"vertical-align:top;width:55px"> <td style=3D"vertical-align:top;width:55px">
<img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m= 532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m=
ax-width:694px" width=3D"45" height=3D"45"> ax-width:694px" width=3D"45" height=3D"45">
</td> </td>
...@@ -121,7 +121,7 @@ nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p> ...@@ -121,7 +121,7 @@ nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p>
<div style=3D"color:#666"> <div style=3D"color:#666">
<p>To respond, reply to this email or visit <a href=3D"http://meta.disc= <p>To respond, reply to this email or visit <a href=3D"http://meta.disc=
ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co= ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co=
lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= lor:#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back=
-up-in-suggested-topics/11005/5</a> in your browser.</p> -up-in-suggested-topics/11005/5</a> in your browser.</p>
</div> </div>
...@@ -132,12 +132,12 @@ lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= ...@@ -132,12 +132,12 @@ lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back=
lpadding=3D"0" border=3D"0"><tbody> lpadding=3D"0" border=3D"0"><tbody>
<tr> <tr>
<td style=3D"vertical-align:top;width:55px"> <td style=3D"vertical-align:top;width:55px">
<img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m= 532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m=
ax-width:694px" width=3D"45" height=3D"45"> ax-width:694px" width=3D"45" height=3D"45">
</td> </td>
<td> <td>
<a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size= <a href=3D"https://meta.discourse.org/users/neil" style=3D"font-size=
:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif;c= :13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif;c=
olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<= olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<=
/a><br> /a><br>
...@@ -155,12 +155,12 @@ vember 19</span> ...@@ -155,12 +155,12 @@ vember 19</span>
adding=3D"0" border=3D"0"><tbody> adding=3D"0" border=3D"0"><tbody>
<tr> <tr>
<td style=3D"vertical-align:top;width:55px"> <td style=3D"vertical-align:top;width:55px">
<img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D= 72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D=
"max-width:694px" width=3D"45" height=3D"45"> "max-width:694px" width=3D"45" height=3D"45">
</td> </td>
<td> <td>
<a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si=
ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif= ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif=
;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= ;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik=
ing</a><br> ing</a><br>
...@@ -173,7 +173,7 @@ vember 19</span> ...@@ -173,7 +173,7 @@ vember 19</span>
<td style=3D"padding-top:5px" colspan=3D"2"> <td style=3D"padding-top:5px" colspan=3D"2">
<p style=3D"margin-top:0"><u></u></p><div> <p style=3D"margin-top:0"><u></u></p><div>
<div></div> <div></div>
<img width=3D"20" height=3D"20" src=3D"http://www.gravatar.com/avatar/51d62= <img width=3D"20" height=3D"20" src=3D"https://www.gravatar.com/avatar/51d62=
3f33f8b83095db84ff35e15dbe8.png?s=3D40&amp;r=3Dpg&amp;d=3Didenticon" style= 3f33f8b83095db84ff35e15dbe8.png?s=3D40&amp;r=3Dpg&amp;d=3Didenticon" style=
=3D"max-width:694px">codinghorror:</div> =3D"max-width:694px">codinghorror:</div>
<blockquote><p style=3D"margin-top:0">I can&#39;t even find that topic by n= <blockquote><p style=3D"margin-top:0">I can&#39;t even find that topic by n=
...@@ -193,12 +193,12 @@ uld be invisible to me, and not showing up in Suggested Topics.</p> ...@@ -193,12 +193,12 @@ uld be invisible to me, and not showing up in Suggested Topics.</p>
adding=3D"0" border=3D"0"><tbody> adding=3D"0" border=3D"0"><tbody>
<tr> <tr>
<td style=3D"vertical-align:top;width:55px"> <td style=3D"vertical-align:top;width:55px">
<img src=3D"http://www.gravatar.com/avatar/51d623f33f8b83095db84ff3= <img src=3D"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff3=
5e15dbe8.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"codinghorror" st= 5e15dbe8.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"codinghorror" st=
yle=3D"max-width:694px" width=3D"45" height=3D"45"> yle=3D"max-width:694px" width=3D"45" height=3D"45">
</td> </td>
<td> <td>
<a href=3D"http://meta.discourse.org/users/codinghorror" style=3D"f= <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"f=
ont-size:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans= ont-size:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans=
-serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan= -serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan=
k">codinghorror</a><br> k">codinghorror</a><br>
...@@ -219,12 +219,12 @@ rout" target=3D"_blank">@eviltrout</a>? I can&#39;t even find that topic by= ...@@ -219,12 +219,12 @@ rout" target=3D"_blank">@eviltrout</a>? I can&#39;t even find that topic by=
adding=3D"0" border=3D"0"><tbody> adding=3D"0" border=3D"0"><tbody>
<tr> <tr>
<td style=3D"vertical-align:top;width:55px"> <td style=3D"vertical-align:top;width:55px">
<img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D= 72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D=
"max-width:694px" width=3D"45" height=3D"45"> "max-width:694px" width=3D"45" height=3D"45">
</td> </td>
<td> <td>
<a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si=
ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif= ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif=
;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= ;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik=
ing</a><br> ing</a><br>
...@@ -241,7 +241,7 @@ lar spam post, and it was promptly deleted/hidden, but it just popped up in= ...@@ -241,7 +241,7 @@ lar spam post, and it was promptly deleted/hidden, but it just popped up in=
<p style=3D"margin-top:0"></p> <p style=3D"margin-top:0"></p>
<div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c= <div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c=
b249e.png" target=3D"_blank"><img src=3D"http://cdn.discourse.org/uploads/m= b249e.png" target=3D"_blank"><img src=3D"https://cdn.discourse.org/uploads/m=
eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig= eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig=
ht=3D"134" style=3D"max-width:694px"><div> ht=3D"134" style=3D"max-width:694px"><div>
...@@ -257,12 +257,12 @@ ht=3D"134" style=3D"max-width:694px"><div> ...@@ -257,12 +257,12 @@ ht=3D"134" style=3D"max-width:694px"><div>
<div style=3D"color:#666"> <div style=3D"color:#666">
<p>To respond, reply to this email or visit <a href=3D"http://meta.discours= <p>To respond, reply to this email or visit <a href=3D"http://meta.discours=
e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:= e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:=
#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back-up-= #666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back-up-=
in-suggested-topics/11005/5</a> in your browser.</p> in-suggested-topics/11005/5</a> in your browser.</p>
</div> </div>
<div style=3D"color:#666"> <div style=3D"color:#666">
<p>To unsubscribe from these emails, visit your <a href=3D"http://meta.disc= <p>To unsubscribe from these emails, visit your <a href=3D"https://meta.disc=
ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre= ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre=
ferences</a>.</p> ferences</a>.</p>
</div> </div>
......
...@@ -117,7 +117,7 @@ describe ApplicationHelper do ...@@ -117,7 +117,7 @@ describe ApplicationHelper do
stub_config_setting(https: false) stub_config_setting(https: false)
expect(helper.gravatar_icon(user_email)) expect(helper.gravatar_icon(user_email))
.to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
end end
it 'uses HTTPs when configured' do it 'uses HTTPs when configured' do
......
...@@ -24,7 +24,7 @@ describe Settings do ...@@ -24,7 +24,7 @@ describe Settings do
expect(described_class.host_without_www('http://foo.com')).to eq 'foo.com' expect(described_class.host_without_www('http://foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('http://www.foo.com')).to eq 'foo.com' expect(described_class.host_without_www('http://www.foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com' expect(described_class.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com'
expect(described_class.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' expect(described_class.host_without_www('https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
expect(described_class.host_without_www('https://foo.com')).to eq 'foo.com' expect(described_class.host_without_www('https://foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('https://www.foo.com')).to eq 'foo.com' expect(described_class.host_without_www('https://www.foo.com')).to eq 'foo.com'
......
...@@ -70,7 +70,7 @@ describe('Environment item', () => { ...@@ -70,7 +70,7 @@ describe('Environment item', () => {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
commit: { commit: {
...@@ -86,7 +86,7 @@ describe('Environment item', () => { ...@@ -86,7 +86,7 @@ describe('Environment item', () => {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"username": "root", "username": "root",
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url": "http://localhost:3000/u/root" "web_url": "http://localhost:3000/u/root"
}, },
"name": "test", "name": "test",
......
...@@ -4,7 +4,7 @@ export default { ...@@ -4,7 +4,7 @@ export default {
for (let i = 0; i < numberUsers; i = i += 1) { for (let i = 0; i < numberUsers; i = i += 1) {
users.push( users.push(
{ {
avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: (i + 1), id: (i + 1),
name: `GitLab User ${i}`, name: `GitLab User ${i}`,
username: `gitlab${i}`, username: `gitlab${i}`,
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
erase_path: '/root/ci-mock/-/jobs/4757/erase', erase_path: '/root/ci-mock/-/jobs/4757/erase',
...@@ -54,7 +54,7 @@ export default { ...@@ -54,7 +54,7 @@ export default {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
active: false, active: false,
...@@ -107,10 +107,10 @@ export default { ...@@ -107,10 +107,10 @@ export default {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', author_gravatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
}, },
......
...@@ -107,7 +107,7 @@ export const note = { ...@@ -107,7 +107,7 @@ export const note = {
"name": "Administrator", "name": "Administrator",
"username": "root", "username": "root",
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"path": "/root" "path": "/root"
}, },
"created_at": "2017-08-10T15:24:03.087Z", "created_at": "2017-08-10T15:24:03.087Z",
......
...@@ -27,7 +27,7 @@ const RESPONSE_MAP = { ...@@ -27,7 +27,7 @@ const RESPONSE_MAP = {
username: 'user0', username: 'user0',
id: 22, id: 22,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0', web_url: 'http: //localhost:3001/user0',
}, },
{ {
...@@ -35,7 +35,7 @@ const RESPONSE_MAP = { ...@@ -35,7 +35,7 @@ const RESPONSE_MAP = {
username: 'tajuana', username: 'tajuana',
id: 18, id: 18,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana', web_url: 'http: //localhost:3001/tajuana',
}, },
{ {
...@@ -43,7 +43,7 @@ const RESPONSE_MAP = { ...@@ -43,7 +43,7 @@ const RESPONSE_MAP = {
username: 'michaele.will', username: 'michaele.will',
id: 16, id: 16,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will', web_url: 'http: //localhost:3001/michaele.will',
}, },
], ],
...@@ -72,24 +72,24 @@ const RESPONSE_MAP = { ...@@ -72,24 +72,24 @@ const RESPONSE_MAP = {
username: 'user0', username: 'user0',
id: 22, id: 22,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0', web_url: 'http://localhost:3001/user0',
}, },
{ {
name: 'Marguerite Bartell', name: 'Marguerite Bartell',
username: 'tajuana', username: 'tajuana',
id: 18, id: 18,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana', web_url: 'http://localhost:3001/tajuana',
}, },
{ {
name: 'Laureen Ritchie', name: 'Laureen Ritchie',
username: 'michaele.will', username: 'michaele.will',
id: 16, id: 16,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will', web_url: 'http://localhost:3001/michaele.will',
}, },
], ],
human_time_estimate: null, human_time_estimate: null,
...@@ -100,24 +100,24 @@ const RESPONSE_MAP = { ...@@ -100,24 +100,24 @@ const RESPONSE_MAP = {
username: 'user0', username: 'user0',
id: 22, id: 22,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0', web_url: 'http://localhost:3001/user0',
}, },
{ {
name: 'Marguerite Bartell', name: 'Marguerite Bartell',
username: 'tajuana', username: 'tajuana',
id: 18, id: 18,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana', web_url: 'http://localhost:3001/tajuana',
}, },
{ {
name: 'Laureen Ritchie', name: 'Laureen Ritchie',
username: 'michaele.will', username: 'michaele.will',
id: 16, id: 16,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will', web_url: 'http://localhost:3001/michaele.will',
}, },
], ],
subscribed: true, subscribed: true,
...@@ -182,7 +182,7 @@ const mockData = { ...@@ -182,7 +182,7 @@ const mockData = {
id: 1, id: 1,
name: 'Administrator', name: 'Administrator',
username: 'root', username: 'root',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
}, },
rootPath: '/', rootPath: '/',
fullPath: '/gitlab-org/gitlab-shell', fullPath: '/gitlab-org/gitlab-shell',
...@@ -194,7 +194,7 @@ const mockData = { ...@@ -194,7 +194,7 @@ const mockData = {
human_total_time_spent: null, human_total_time_spent: null,
}, },
user: { user: {
avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 1, id: 1,
name: 'Administrator', name: 'Administrator',
username: 'root', username: 'root',
......
...@@ -6,14 +6,14 @@ const ASSIGNEE = { ...@@ -6,14 +6,14 @@ const ASSIGNEE = {
id: 2, id: 2,
name: 'gitlab user 2', name: 'gitlab user 2',
username: 'gitlab2', username: 'gitlab2',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
}; };
const ANOTHER_ASSINEE = { const ANOTHER_ASSINEE = {
id: 3, id: 3,
name: 'gitlab user 3', name: 'gitlab user 3',
username: 'gitlab3', username: 'gitlab3',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
}; };
const PARTICIPANT = { const PARTICIPANT = {
...@@ -38,7 +38,7 @@ describe('Sidebar store', () => { ...@@ -38,7 +38,7 @@ describe('Sidebar store', () => {
id: 1, id: 1,
name: 'Administrator', name: 'Administrator',
username: 'root', username: 'root',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
}, },
editable: true, editable: true,
rootPath: '/', rootPath: '/',
......
import Vue from 'vue';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MR widget status icon component', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(mrStatusIcon);
});
afterEach(() => {
vm.$destroy();
});
describe('while loading', () => {
it('renders loading icon', () => {
vm = mountComponent(Component, { status: 'loading' });
expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
});
});
describe('with status icon', () => {
it('renders ci status icon', () => {
vm = mountComponent(Component, { status: 'failed' });
expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull();
});
});
describe('with disabled button', () => {
it('renders a disabled button', () => {
vm = mountComponent(Component, { status: 'failed', showDisabledButton: true });
expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge');
});
});
describe('without disabled button', () => {
it('does not render a disabled button', () => {
vm = mountComponent(Component, { status: 'failed' });
expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull();
});
});
});
import Vue from 'vue';
import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging';
describe('MRWidgetMerging', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = mergingComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
});
describe('template', () => {
it('should have correct elements', () => {
const Component = Vue.extend(mergingComponent);
const mr = {
targetBranchPath: '/branch-path',
targetBranch: 'branch',
};
const el = new Component({
el: document.createElement('div'),
propsData: { mr },
}).$el;
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('This merge request is in the process of being merged');
expect(el.innerText).toContain('changes will be merged into');
expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch);
});
});
});
import Vue from 'vue';
import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetMerging', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(mergingComponent);
vm = mountComponent(Component, { mr: {
targetBranchPath: '/branch-path',
targetBranch: 'branch',
} });
});
afterEach(() => {
vm.$destroy();
});
it('renders information about merge request being merged', () => {
expect(
vm.$el.querySelector('.media-body').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '),
).toContain('This merge request is in the process of being merged');
});
it('renders branch information', () => {
expect(
vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '),
).toEqual('The changes will be merged into branch');
expect(
vm.$el.querySelector('a').getAttribute('href'),
).toEqual('/branch-path');
});
});
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
"username": "root", "username": "root",
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root" "web_url": "http://localhost:3000/root"
}, },
"merged_at": "2017-04-07T15:39:25.696Z", "merged_at": "2017-04-07T15:39:25.696Z",
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
"username": "root", "username": "root",
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root" "web_url": "http://localhost:3000/root"
}, },
"merge_user": null, "merge_user": null,
...@@ -64,7 +64,7 @@ export default { ...@@ -64,7 +64,7 @@ export default {
"username": "root", "username": "root",
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root" "web_url": "http://localhost:3000/root"
}, },
"active": false, "active": false,
...@@ -159,10 +159,10 @@ export default { ...@@ -159,10 +159,10 @@ export default {
"username": "root", "username": "root",
"id": 1, "id": 1,
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root" "web_url": "http://localhost:3000/root"
}, },
"author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d", "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
"commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d" "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
}, },
......
require 'spec_helper'
describe Gitlab::Git::AttributesAtRefParser, seed_helper: true do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
subject { described_class.new(repository, 'lfs') }
it 'loads .gitattributes blob' do
repository.raw # Initialize repository in advance since this also checks attributes
expected_filter = 'filter=lfs diff=lfs merge=lfs'
receive_blob = receive(:new).with(a_string_including(expected_filter))
expect(Gitlab::Git::AttributesParser).to receive_blob.and_call_original
subject
end
it 'handles missing blobs' do
expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error
end
describe '#attributes' do
it 'returns the attributes as a Hash' do
expect(subject.attributes('test.lfs')['filter']).to eq('lfs')
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Git::Attributes, seed_helper: true do describe Gitlab::Git::AttributesParser, seed_helper: true do
let(:path) do let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') }
File.join(SEED_STORAGE_PATH, 'with-git-attributes.git') let(:data) { File.read(attributes_path) }
end
subject { described_class.new(path) } subject { described_class.new(data) }
describe '#attributes' do describe '#attributes' do
context 'using a path with attributes' do context 'using a path with attributes' do
...@@ -66,6 +65,26 @@ describe Gitlab::Git::Attributes, seed_helper: true do ...@@ -66,6 +65,26 @@ describe Gitlab::Git::Attributes, seed_helper: true do
expect(subject.attributes('test.foo')).to eq({}) expect(subject.attributes('test.foo')).to eq({})
end end
end end
context 'when attributes data is a file handle' do
subject do
File.open(attributes_path, 'r') do |file_handle|
described_class.new(file_handle)
end
end
it 'returns the attributes as a Hash' do
expect(subject.attributes('test.txt')).to eq({ 'text' => true })
end
end
context 'when attributes data is nil' do
let(:data) { nil }
it 'returns an empty Hash' do
expect(subject.attributes('test.foo')).to eq({})
end
end
end end
describe '#patterns' do describe '#patterns' do
...@@ -74,14 +93,14 @@ describe Gitlab::Git::Attributes, seed_helper: true do ...@@ -74,14 +93,14 @@ describe Gitlab::Git::Attributes, seed_helper: true do
end end
it 'parses an entry that uses a tab to separate the pattern and attributes' do it 'parses an entry that uses a tab to separate the pattern and attributes' do
expect(subject.patterns[File.join(path, '*.md')]) expect(subject.patterns[File.join('/', '*.md')])
.to eq({ 'gitlab-language' => 'markdown' }) .to eq({ 'gitlab-language' => 'markdown' })
end end
it 'stores patterns in reverse order' do it 'stores patterns in reverse order' do
first = subject.patterns.to_a[0] first = subject.patterns.to_a[0]
expect(first[0]).to eq(File.join(path, 'bla/bla.txt')) expect(first[0]).to eq(File.join('/', 'bla/bla.txt'))
end end
# It's a bit hard to test for something _not_ being processed. As such we'll # It's a bit hard to test for something _not_ being processed. As such we'll
...@@ -89,14 +108,6 @@ describe Gitlab::Git::Attributes, seed_helper: true do ...@@ -89,14 +108,6 @@ describe Gitlab::Git::Attributes, seed_helper: true do
it 'ignores any comments and empty lines' do it 'ignores any comments and empty lines' do
expect(subject.patterns.length).to eq(10) expect(subject.patterns.length).to eq(10)
end end
it 'does not parse anything when the attributes file does not exist' do
expect(File).to receive(:exist?)
.with(File.join(path, 'info/attributes'))
.and_return(false)
expect(subject.patterns).to eq({})
end
end end
describe '#parse_attributes' do describe '#parse_attributes' do
...@@ -132,17 +143,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do ...@@ -132,17 +143,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
end end
it 'does not yield when the attributes file does not exist' do
expect(File).to receive(:exist?)
.with(File.join(path, 'info/attributes'))
.and_return(false)
expect { |b| subject.each_line(&b) }.not_to yield_control
end
it 'does not yield when the attributes file has an unsupported encoding' do it 'does not yield when the attributes file has an unsupported encoding' do
path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git') path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info', 'attributes')
attrs = described_class.new(path) attrs = described_class.new(File.read(path))
expect { |b| attrs.each_line(&b) }.not_to yield_control expect { |b| attrs.each_line(&b) }.not_to yield_control
end end
......
require 'spec_helper'
describe Gitlab::Git::InfoAttributes, seed_helper: true do
let(:path) do
File.join(SEED_STORAGE_PATH, 'with-git-attributes.git')
end
subject { described_class.new(path) }
describe '#attributes' do
context 'using a path with attributes' do
it 'returns the attributes as a Hash' do
expect(subject.attributes('test.txt')).to eq({ 'text' => true })
end
it 'returns an empty Hash for a defined path without attributes' do
expect(subject.attributes('bla/bla.txt')).to eq({})
end
end
end
describe '#parser' do
it 'parses a file with entries' do
expect(subject.patterns).to be_an_instance_of(Hash)
expect(subject.patterns["/*.txt"]).to eq({ 'text' => true })
end
it 'does not parse anything when the attributes file does not exist' do
expect(File).to receive(:exist?)
.with(File.join(path, 'info/attributes'))
.and_return(false)
expect(subject.patterns).to eq({})
end
it 'does not parse attributes files with unsupported encoding' do
path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git')
subject = described_class.new(path)
expect(subject.patterns).to eq({})
end
end
end
...@@ -899,6 +899,44 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -899,6 +899,44 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
context "compare results between log_by_walk and log_by_shell" do
let(:options) { { ref: "master" } }
let(:commits_by_walk) { repository.log(options).map(&:id) }
let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) }
it { expect(commits_by_walk).to eq(commits_by_shell) }
context "with limit" do
let(:options) { { ref: "master", limit: 1 } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
context "with offset" do
let(:options) { { ref: "master", offset: 1 } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
context "with skip_merges" do
let(:options) { { ref: "master", skip_merges: true } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
context "with path" do
let(:options) { { ref: "master", path: "encoding" } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
context "with follow" do
let(:options) { { ref: "master", path: "encoding", follow: true } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
end
end
context "where provides 'after' timestamp" do context "where provides 'after' timestamp" do
options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
......
...@@ -131,6 +131,29 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -131,6 +131,29 @@ describe Gitlab::GitalyClient::CommitService do
end end
end end
describe '#commit_count' do
before do
expect_any_instance_of(Gitaly::CommitService::Stub)
.to receive(:count_commits)
.with(gitaly_request_with_path(storage_name, relative_path),
kind_of(Hash))
.and_return([])
end
it 'sends a commit_count message' do
client.commit_count(revision)
end
context 'with UTF-8 params strings' do
let(:revision) { "branch\u011F" }
let(:path) { "foo/\u011F.txt" }
it 'handles string encodings correctly' do
client.commit_count(revision, path: path)
end
end
end
describe '#find_commit' do describe '#find_commit' do
let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' } let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' }
it 'sends an RPC request' do it 'sends an RPC request' do
......
require 'spec_helper'
describe Gitlab::GitalyClient::HealthCheckService do
let(:project) { create(:project) }
let(:storage_name) { project.repository_storage }
subject { described_class.new(storage_name) }
describe '#check' do
it 'successfully sends a health check request' do
expect(Gitlab::GitalyClient).to receive(:call).with(
storage_name,
:health_check,
:check,
instance_of(Grpc::Health::V1::HealthCheckRequest),
timeout: Gitlab::GitalyClient.fast_timeout).and_call_original
expect(subject.check).to eq({ success: true })
end
it 'receives an unsuccessful health check request' do
expect_any_instance_of(Grpc::Health::V1::Health::Stub)
.to receive(:check)
.and_return(double(status: false))
expect(subject.check).to eq({ success: false })
end
it 'gracefully handles gRPC error' do
expect(Gitlab::GitalyClient).to receive(:call).with(
storage_name,
:health_check,
:check,
instance_of(Grpc::Health::V1::HealthCheckRequest),
timeout: Gitlab::GitalyClient.fast_timeout)
.and_raise(GRPC::Unavailable.new('Connection refused'))
expect(subject.check).to eq({ success: false, message: '14:Connection refused' })
end
end
end
...@@ -3,6 +3,31 @@ require 'spec_helper' ...@@ -3,6 +3,31 @@ require 'spec_helper'
# We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want # We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want
# those stubs while testing the GitalyClient itself. # those stubs while testing the GitalyClient itself.
describe Gitlab::GitalyClient, skip_gitaly_mock: true do describe Gitlab::GitalyClient, skip_gitaly_mock: true do
describe '.stub_class' do
it 'returns the gRPC health check stub' do
expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub)
end
it 'returns a Gitaly stub' do
expect(described_class.stub_class(:ref_service)).to eq(::Gitaly::RefService::Stub)
end
end
describe '.stub_address' do
it 'returns the same result after being called multiple times' do
address = 'localhost:9876'
prefixed_address = "tcp://#{address}"
allow(Gitlab.config.repositories).to receive(:storages).and_return({
'default' => { 'gitaly_address' => prefixed_address }
})
2.times do
expect(described_class.stub_address('default')).to eq('localhost:9876')
end
end
end
describe '.stub' do describe '.stub' do
# Notice that this is referring to gRPC "stubs", not rspec stubs # Notice that this is referring to gRPC "stubs", not rspec stubs
before do before do
......
require 'spec_helper'
describe Gitlab::HealthChecks::GitalyCheck do
let(:result_class) { Gitlab::HealthChecks::Result }
let(:repository_storages) { ['default'] }
before do
allow(described_class).to receive(:repository_storages) { repository_storages }
end
describe '#readiness' do
subject { described_class.readiness }
before do
expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
end
context 'Gitaly server is up' do
let(:gitaly_check) { double(check: { success: true }) }
it { is_expected.to eq([result_class.new(true, nil, shard: 'default')]) }
end
context 'Gitaly server is down' do
let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
it { is_expected.to eq([result_class.new(false, 'Connection refused', shard: 'default')]) }
end
end
describe '#metrics' do
subject { described_class.metrics }
before do
expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
end
context 'Gitaly server is up' do
let(:gitaly_check) { double(check: { success: true }) }
it 'provides metrics' do
expect(subject).to all(have_attributes(labels: { shard: 'default' }))
expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 1))
expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0))
end
end
context 'Gitaly server is down' do
let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
it 'provides metrics' do
expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0))
expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0))
end
end
end
end
...@@ -1336,9 +1336,19 @@ describe MergeRequest do ...@@ -1336,9 +1336,19 @@ describe MergeRequest do
end end
end end
context 'when the revert commit is mentioned in a note before the MR was merged' do context 'when the revert commit is mentioned in a note just before the MR was merged' do
before do before do
subject.notes.last.update!(created_at: subject.metrics.merged_at - 1.second) subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds)
end
it 'returns false' do
expect(subject.can_be_reverted?(current_user)).to be_falsey
end
end
context 'when the revert commit is mentioned in a note long before the MR was merged' do
before do
subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes)
end end
it 'returns true' do it 'returns true' do
......
...@@ -20,7 +20,7 @@ module TestEnv ...@@ -20,7 +20,7 @@ module TestEnv
'improve/awesome' => '5937ac0', 'improve/awesome' => '5937ac0',
'merged-target' => '21751bf', 'merged-target' => '21751bf',
'markdown' => '0ed8c6c', 'markdown' => '0ed8c6c',
'lfs' => 'be93687', 'lfs' => '55bc176',
'master' => 'b83d6e3', 'master' => 'b83d6e3',
'merge-test' => '5937ac0', 'merge-test' => '5937ac0',
"'test'" => 'e56497b', "'test'" => 'e56497b',
......
...@@ -13,8 +13,8 @@ describe 'projects/pipelines_settings/_show' do ...@@ -13,8 +13,8 @@ describe 'projects/pipelines_settings/_show' do
render render
expect(rendered).to have_css('.settings-message') expect(rendered).to have_css('.settings-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the') expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
expect(rendered).to have_link('Kubernetes service') expect(rendered).to have_link('Kubernetes cluster')
end end
end end
...@@ -27,8 +27,8 @@ describe 'projects/pipelines_settings/_show' do ...@@ -27,8 +27,8 @@ describe 'projects/pipelines_settings/_show' do
render render
expect(rendered).to have_css('.settings-message') expect(rendered).to have_css('.settings-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the') expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
expect(rendered).to have_link('Kubernetes service') expect(rendered).to have_link('Kubernetes cluster')
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment