Commit eaca44e2 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 4310-security-reports

* master: (76 commits)
  Add changelog entry
  Include mock replication slot WAL
  Hide sync status when event timestamp is not present
  Show empty state when timestamp is not present
  Update common.rb
  Merge branch 'qa-secret-variables-scenario' into tmp
  Just don't expand if it's already expanded
  Use `numberToHumanSize` for memory unit conversion
  Add multiple steps to limitations.
  Resolve scripts/lint-rugged conflicts
  fix bug in webpack_helper in which force_same_domain argument was ignored breaking local rspec tests
  Merge branch 'sh-fix-geo-migration-path' into 'master'
  Fix rubocop offenses introduced in !16623
  Fix spec/ee/spec/lib/gitlab/geo/health_check_spec.rb
  fix documentation about node version
  Ensure the  job also run for tags
  Fix broken paths
  Fix conflicts for app/assets/javascripts/vue_merge_request_widget/dependencies.js
  Fix conflicts for app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
  Fix conflicts for app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
  ...
parents 405b6954 c3efd8e0
...@@ -413,7 +413,6 @@ spinach-mysql 2 3: *spinach-metadata-mysql ...@@ -413,7 +413,6 @@ spinach-mysql 2 3: *spinach-metadata-mysql
# Static analysis jobs # Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis .ruby-static-analysis: &ruby-static-analysis
<<: *pull-cache
variables: variables:
SIMPLECOV: "false" SIMPLECOV: "false"
SETUP_DB: "false" SETUP_DB: "false"
...@@ -434,6 +433,12 @@ static-analysis: ...@@ -434,6 +433,12 @@ static-analysis:
stage: test stage: test
script: script:
- scripts/static-analysis - scripts/static-analysis
cache:
key: "ruby-2.3.6-with-yarn-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
- tmp/rubocop_cache
# Documentation checks: # Documentation checks:
# - Check validity of relative links # - Check validity of relative links
...@@ -742,8 +747,6 @@ pages: ...@@ -742,8 +747,6 @@ pages:
cache gems: cache gems:
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
only:
- tags
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
script: script:
...@@ -754,6 +757,7 @@ cache gems: ...@@ -754,6 +757,7 @@ cache gems:
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
- tags
gitlab_git_test: gitlab_git_test:
<<: *dedicated-runner <<: *dedicated-runner
......
...@@ -18,6 +18,7 @@ AllCops: ...@@ -18,6 +18,7 @@ AllCops:
- 'bin/**/*' - 'bin/**/*'
- 'generator_templates/**/*' - 'generator_templates/**/*'
- 'builds/**/*' - 'builds/**/*'
CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a # This cop checks whether some constant value isn't a
# mutable literal (e.g. array or hash). # mutable literal (e.g. array or hash).
......
...@@ -422,7 +422,7 @@ group :ed25519 do ...@@ -422,7 +422,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.76.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.78.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -309,7 +309,7 @@ GEM ...@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.76.0) gitaly-proto (0.78.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1091,7 +1091,7 @@ DEPENDENCIES ...@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.76.0) gitaly-proto (~> 0.78.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
...@@ -180,7 +180,7 @@ const Api = { ...@@ -180,7 +180,7 @@ const Api = {
issueTemplate(namespacePath, projectPath, key, type, callback) { issueTemplate(namespacePath, projectPath, key, type, callback) {
const url = Api.buildUrl(Api.issuableTemplatePath) const url = Api.buildUrl(Api.issuableTemplatePath)
.replace(':key', key) .replace(':key', encodeURIComponent(key))
.replace(':type', type) .replace(':type', type)
.replace(':project_path', projectPath) .replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath); .replace(':namespace_path', namespacePath);
......
...@@ -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>
......
...@@ -18,7 +18,7 @@ function renderWithKaTeX(elements) { ...@@ -18,7 +18,7 @@ function renderWithKaTeX(elements) {
const display = $this.attr('data-math-style') === 'display'; const display = $this.attr('data-math-style') === 'display';
try { try {
katex.render($this.text(), mathNode.get(0), { displayMode: display }); katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false });
mathNode.insertAfter($this); mathNode.insertAfter($this);
$this.remove(); $this.remove();
} catch (err) { } catch (err) {
......
...@@ -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>
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
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';
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetClosed', name: 'MRWidgetClosed',
......
<script> <script>
import statusIcon from '../mr_widget_status_icon'; import statusIcon from '../mr_widget_status_icon.vue';
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,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li ...@@ -19,7 +19,7 @@ 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';
export { default as MergingState } from './components/states/mr_widget_merging'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
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';
......
...@@ -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.')
......
...@@ -2,7 +2,7 @@ require 'webpack/rails/manifest' ...@@ -2,7 +2,7 @@ require 'webpack/rails/manifest'
module WebpackHelper module WebpackHelper
def webpack_bundle_tag(bundle, force_same_domain: false) def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true)) javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
end end
# override webpack-rails gem helper until changes can make it upstream # override webpack-rails gem helper until changes can make it upstream
......
...@@ -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)
......
...@@ -25,7 +25,7 @@ class Repository ...@@ -25,7 +25,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, to: :raw_repository delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository
CreateTreeError = Class.new(StandardError) CreateTreeError = Class.new(StandardError)
......
...@@ -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
......
...@@ -9,15 +9,15 @@ ...@@ -9,15 +9,15 @@
- else - else
.row-content-block.second-block.center .row-content-block.second-block.center
%h3.page-title %h4
This project does not have a README yet This project does not have a README yet
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
%p %p
A A
%code README %code README
file contains information about other files in a repository and is commonly file contains information about other files in a repository and is commonly
distributed with computer software, forming part of its documentation. distributed with computer software, forming part of its documentation.
GitLab will render it here instead of this message.
%p %p
We recommend you to = link_to "Add Readme", add_special_file_path(@project, file_name: 'README.md'), class: 'btn btn-new'
= link_to "add a README", add_special_file_path(@project, file_name: 'README.md')
file to the repository and GitLab will render it here instead of this message.
...@@ -6,7 +6,10 @@ ...@@ -6,7 +6,10 @@
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project) - can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- can_create_snippet = can?(current_user, :create_snippet, @project) - can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
- if can_create_issue - if can_create_issue
%li= link_to _('New issue'), new_project_issue_path(@project) %li= link_to _('New issue'), new_project_issue_path(@project)
...@@ -14,11 +17,11 @@ ...@@ -14,11 +17,11 @@
- if merge_project - if merge_project
%li= link_to _('New merge request'), project_new_merge_request_path(merge_project) %li= link_to _('New merge request'), project_new_merge_request_path(merge_project)
- if can_create_snippet - if can_create_project_snippet
%li= link_to _('New snippet'), new_project_snippet_path(@project) %li= link_to _('New snippet'), new_project_snippet_path(@project)
- if can_create_issue || merge_project || can_create_snippet - if can?(current_user, :push_code, @project)
%li.divider %li.dropdown-header= _('This repository')
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
......
- 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
...@@ -6,8 +6,9 @@ ...@@ -6,8 +6,9 @@
= render "home_panel" = render "home_panel"
.row-content-block.second-block.center .row-content-block.second-block.center
%h3.page-title %h4
The repository for this project is empty The repository for this project is empty
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
%p %p
If you already have files you can push them using command line instructions below. If you already have files you can push them using command line instructions below.
...@@ -28,8 +29,8 @@ ...@@ -28,8 +29,8 @@
%p %p
- link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
= s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link } = s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link }
%p %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master'), class: 'btn btn-new'
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
%div{ class: container_class } %div{ class: container_class }
...@@ -79,4 +80,4 @@ ...@@ -79,4 +80,4 @@
- if can? current_user, :remove_project, @project - if can? current_user, :remove_project, @project
.prepend-top-20 .prepend-top-20
= link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
<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>
...@@ -58,15 +58,15 @@ ...@@ -58,15 +58,15 @@
= icon('skype') = icon('skype')
- unless @user.linkedin.blank? - unless @user.linkedin.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= link_to linkedin_url(@user), title: "LinkedIn" do = link_to linkedin_url(@user), title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('linkedin-square') = icon('linkedin-square')
- unless @user.twitter.blank? - unless @user.twitter.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= link_to twitter_url(@user), title: "Twitter" do = link_to twitter_url(@user), title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('twitter-square') = icon('twitter-square')
- unless @user.website_url.blank? - unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link' = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'noopener noreferrer nofollow'
- unless @user.location.blank? - unless @user.location.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= icon('map-marker') = icon('map-marker')
......
---
title: Handle empty event timestamp and larger memory units
merge_request: 4206
author:
type: fixed
---
title: Handle special characters on API request of issuable templates
merge_request: 15323
author: Takuya Noguchi
type: fixed
---
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: Disable throwOnError in KaTeX to reveal user where is the problem
merge_request: 16684
author: Jakub Jirutka
type: other
---
title: Improve empty project overview
merge_request: 16617
author: George Tsiolis
type: added
---
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: fix documentation about node version
merge_request: 16720
author: Tobias Gurtzick
type: other
---
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'])
......
...@@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do
# #
# Templates # Templates
# #
get '/templates/:template_type/:key' => 'templates#show', as: :template get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: /[^\/]+/ }
resource :avatar, only: [:show, :destroy] resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
......
...@@ -40,7 +40,6 @@ ...@@ -40,7 +40,6 @@
- [upload_checksum, 1] - [upload_checksum, 1]
- [repository_fork, 1] - [repository_fork, 1]
- [repository_import, 1] - [repository_import, 1]
- [repository_remove_remote, 1]
- [github_importer, 1] - [github_importer, 1]
- [github_import_advance_stage, 1] - [github_import_advance_stage, 1]
- [project_service, 1] - [project_service, 1]
...@@ -72,6 +71,7 @@ ...@@ -72,6 +71,7 @@
# EE-specific queues # EE-specific queues
- [ldap_group_sync, 2] - [ldap_group_sync, 2]
- [geo, 1] - [geo, 1]
- [repository_remove_remote, 1]
- [repository_update_mirror, 1] - [repository_update_mirror, 1]
- [repository_update_remote_mirror, 1] - [repository_update_remote_mirror, 1]
- [project_update_repository_storage, 1] - [project_update_repository_storage, 1]
......
> 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.
......
...@@ -197,12 +197,11 @@ Read how to [replicate the Container Registry](docker_registry.md). ...@@ -197,12 +197,11 @@ Read how to [replicate the Container Registry](docker_registry.md).
## Current limitations ## Current limitations
- You cannot push code to secondary nodes - You cannot push code to secondary nodes, see [3912](https://gitlab.com/gitlab-org/gitlab-ee/issues/3912) for details.
- The primary node has to be online for OAuth login to happen (existing - The primary node has to be online for OAuth login to happen (existing sessions and Git are not affected)
sessions and Git are not affected) - It works for repos, wikis, issues, and merge requests, but it does not work for job logs, artifacts, GitLab Pages, and Docker images of the Container
- It works for repos, wikis, issues, and merge requests
- It does not work for job logs, artifacts, GitLab Pages, and Docker images of the Container
Registry (by default, but you can configure it separately, see [replicate the Container Registry](docker_registry.md) for details) Registry (by default, but you can configure it separately, see [replicate the Container Registry](docker_registry.md) for details)
- The installation takes multiple manual steps that together can take about an hour depending on circumstances; we are working on improving this experience, see [#2978](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2978) for details.
## Frequently Asked Questions ## Frequently Asked Questions
......
...@@ -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
......
...@@ -54,17 +54,16 @@ sudo gem install bundler --no-ri --no-rdoc ...@@ -54,17 +54,16 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Update Node ### 4. Update Node
GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets.
it has a minimum requirement of node v4.3.0. We require a minimum version of node v6.0.0.
You can check which version you are running with `node -v`. If you are running You can check which version you are running with `node -v`. If you are running
a version older than `v4.3.0` you will need to update to a newer version. You a version older than `v6.0.0` you will need to update to a newer version. You
can find instructions to install from community maintained packages or compile can find instructions to install from community maintained packages or compile
from source at the nodejs.org website. from source at the nodejs.org website.
<https://nodejs.org/en/download/> <https://nodejs.org/en/download/>
Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies. JavaScript dependencies.
......
...@@ -56,17 +56,16 @@ sudo gem install bundler --no-ri --no-rdoc ...@@ -56,17 +56,16 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Update Node ### 4. Update Node
GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets.
it has a minimum requirement of node v4.3.0. We require a minimum version of node v6.0.0.
You can check which version you are running with `node -v`. If you are running You can check which version you are running with `node -v`. If you are running
a version older than `v4.3.0` you will need to update to a newer version. You a version older than `v6.0.0` you will need to update to a newer version. You
can find instructions to install from community maintained packages or compile can find instructions to install from community maintained packages or compile
from source at the nodejs.org website. from source at the nodejs.org website.
<https://nodejs.org/en/download/> <https://nodejs.org/en/download/>
Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies. JavaScript dependencies.
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* eslint-disable vue/no-side-effects-in-computed-properties */ /* eslint-disable vue/no-side-effects-in-computed-properties */
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { parseSeconds, stringifyTime } from '~/lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from '~/lib/utils/pretty_time';
import { bytesToMiB } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import { VALUE_TYPE, CUSTOM_TYPE } from '../constants'; import { VALUE_TYPE, CUSTOM_TYPE } from '../constants';
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
return `${this.nodeDetails.version} (${this.nodeDetails.revision})`; return `${this.nodeDetails.version} (${this.nodeDetails.revision})`;
}, },
replicationSlotWAL() { replicationSlotWAL() {
return `${bytesToMiB(this.nodeDetails.replicationSlotWAL)} MB`; return numberToHumanSize(this.nodeDetails.replicationSlotWAL);
}, },
dbReplicationLag() { dbReplicationLag() {
// Replication lag can be nil if the secondary isn't actually streaming // Replication lag can be nil if the secondary isn't actually streaming
......
...@@ -35,16 +35,24 @@ ...@@ -35,16 +35,24 @@
<div <div
class="node-detail-value" class="node-detail-value"
> >
<template v-if="eventTimeStamp">
<strong> <strong>
{{ eventId }} {{ eventId }}
</strong> </strong>
<span <span
v-tooltip v-tooltip
v-if="eventTimeStamp"
class="event-status-timestamp" class="event-status-timestamp"
data-placement="bottom" data-placement="bottom"
:title="timeStampString" :title="timeStampString"
> >
({{ timeFormated(timeStamp) }}) ({{ timeFormated(timeStamp) }})
</span> </span>
</template>
<strong
v-else
>
{{ __('Not available') }}
</strong>
</div> </div>
</template> </template>
...@@ -32,6 +32,9 @@ ...@@ -32,6 +32,9 @@
syncType() { syncType() {
return this.namespaces.length > 0 ? s__('GeoNodes|Selective') : s__('GeoNodes|Full'); return this.namespaces.length > 0 ? s__('GeoNodes|Selective') : s__('GeoNodes|Full');
}, },
eventTimestampEmpty() {
return this.lastEvent.timeStamp === 0 || this.cursorLastEvent.timeStamp === 0;
},
syncLagInSeconds() { syncLagInSeconds() {
return this.lagInSeconds(this.lastEvent.timeStamp, this.cursorLastEvent.timeStamp); return this.lagInSeconds(this.lastEvent.timeStamp, this.cursorLastEvent.timeStamp);
}, },
...@@ -79,7 +82,8 @@ ...@@ -79,7 +82,8 @@
return `${timeAgoStr} (${pendingEvents} events)`; return `${timeAgoStr} (${pendingEvents} events)`;
}, },
statusTooltip(lagInSeconds) { statusTooltip(lagInSeconds) {
if (lagInSeconds <= TIME_DIFF.FIVE_MINS) { if (this.eventTimestampEmpty ||
lagInSeconds <= TIME_DIFF.FIVE_MINS) {
return ''; return '';
} else if (lagInSeconds > TIME_DIFF.FIVE_MINS && } else if (lagInSeconds > TIME_DIFF.FIVE_MINS &&
lagInSeconds <= TIME_DIFF.HOUR) { lagInSeconds <= TIME_DIFF.HOUR) {
...@@ -107,6 +111,7 @@ ...@@ -107,6 +111,7 @@
css-classes="sync-status-icon prepend-left-5" css-classes="sync-status-icon prepend-left-5"
/> />
<span <span
v-if="!eventTimestampEmpty"
class="sync-status-event-info prepend-left-5" class="sync-status-event-info prepend-left-5"
> >
{{ syncStatusEventInfo }} {{ syncStatusEventInfo }}
......
...@@ -74,11 +74,11 @@ export default class GeoNodesStore { ...@@ -74,11 +74,11 @@ export default class GeoNodesStore {
failureCount: rawNodeDetails.attachments_failed_count || 0, failureCount: rawNodeDetails.attachments_failed_count || 0,
}, },
lastEvent: { lastEvent: {
id: rawNodeDetails.last_event_id, id: rawNodeDetails.last_event_id || 0,
timeStamp: rawNodeDetails.last_event_timestamp, timeStamp: rawNodeDetails.last_event_timestamp,
}, },
cursorLastEvent: { cursorLastEvent: {
id: rawNodeDetails.cursor_last_event_id, id: rawNodeDetails.cursor_last_event_id || 0,
timeStamp: rawNodeDetails.cursor_last_event_timestamp, timeStamp: rawNodeDetails.cursor_last_event_timestamp,
}, },
namespaces: rawNodeDetails.namespaces, namespaces: rawNodeDetails.namespaces,
......
import Flash from '~/flash'; import Flash from '~/flash';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon'; import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import ApprovalsBody from './approvals_body'; import ApprovalsBody from './approvals_body';
import ApprovalsFooter from './approvals_footer'; import ApprovalsFooter from './approvals_footer';
......
<script> <script>
import { __ } from '~/locale'; /* eslint-disable vue/require-default-prop */
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon'; import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import issuesBlock from './mr_widget_report_issues.vue'; import issuesBlock from './mr_widget_report_issues.vue';
......
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon'; import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
export default { export default {
props: { props: {
......
...@@ -24,7 +24,7 @@ module Gitlab ...@@ -24,7 +24,7 @@ module Gitlab
def self.db_migrate_path def self.db_migrate_path
# Lazy initialisation so Rails.root will be defined # Lazy initialisation so Rails.root will be defined
@db_migrate_path ||= File.join(Rails.root, 'db', 'geo', 'migrate') @db_migrate_path ||= File.join(Rails.root, 'ee', 'db', 'geo', 'migrate')
end end
def self.get_database_version def self.get_database_version
......
class Spinach::Features::User < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedUser
include SharedProject
step 'I should see user "John Doe" page' do
expect(title).to match(/^\s*John Doe/)
end
step 'I visit unsubscribe link' do
email = Base64.urlsafe_encode64("joh@doe.org")
visit "/unsubscribes/#{email}"
end
step 'I should see unsubscribe text and button' do
expect(page).to have_content "Unsubscribe from Admin notifications Yes, I want to unsubscribe joh@doe.org from any further admin emails."
end
step 'I press the unsubscribe button' do
click_button("Unsubscribe")
end
step 'I should be unsubscribed' do
expect(current_path).to eq root_path
end
step '"John Doe" has contributions' do
user = User.find_by(name: 'John Doe')
project = contributed_project
# Issue contribution
issue_params = { title: 'Bug in old browser' }
Issues::CreateService.new(project, user, issue_params).execute
# Push code contribution
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event, commit_count: 3)
end
step 'I should see contributed projects' do
page.within '#contributed' do
expect(page).to have_content(@contributed_project.name)
end
end
step 'I should see contributions calendar' do
expect(page).to have_css('.js-contrib-calendar')
end
def contributed_project
@contributed_project ||= create(:project, :public, :empty_repo)
end
end
Feature: User
Background:
Given User "John Doe" exists
And "John Doe" owns private project "Enterprise"
# Signed out
@javascript
Scenario: I visit user "John Doe" page while not signed in when he owns a public project
Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community"
When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should not see project "Internal"
And I should see project "Community"
# Signed in as someone else
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project
Given "John Doe" owns public project "Community"
And "John Doe" owns internal project "Internal"
And I sign in as a user
When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should see project "Internal"
And I should see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project
Given "John Doe" owns internal project "Internal"
And I sign in as a user
When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should see project "Internal"
And I should not see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see
Given I sign in as a user
When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should not see project "Internal"
And I should not see project "Community"
# Signed in as the user himself
@javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project
Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community"
And I sign in as "John Doe"
When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should see project "Enterprise"
And I should see project "Internal"
And I should see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project
Given I sign in as "John Doe"
When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should see project "Enterprise"
And I should not see project "Internal"
And I should not see project "Community"
Scenario: I unsubscribe from admin notifications
Given I sign in as "John Doe"
When I visit unsubscribe link
Then I should see unsubscribe text and button
And I press the unsubscribe button
Then I should be unsubscribed
@javascript
Scenario: "John Doe" contribution profile
Given I sign in as a user
And "John Doe" has contributions
When I visit user "John Doe" page
And I click on "Contributed projects" tab
Then I should see user "John Doe" page
And I should see contributed projects
And I should see contributions calendar
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,18 +82,12 @@ module Gitlab ...@@ -98,18 +82,12 @@ 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|
return unless File.exist?(full_path)
File.open(full_path, 'r') do |handle|
handle.each_line do |line|
break unless line.valid_encoding? break unless line.valid_encoding?
yield line.strip yield line.strip
end end
end end
end
private private
...@@ -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.
......
...@@ -70,11 +70,9 @@ module Gitlab ...@@ -70,11 +70,9 @@ module Gitlab
# Returns array of Gitlab::Git::Blob # Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set # Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids) def batch_lfs_pointers(repository, blob_ids)
return [] if blob_ids.empty?
repository.gitaly_migrate(:batch_lfs_pointers) do |is_enabled| repository.gitaly_migrate(:batch_lfs_pointers) do |is_enabled|
if is_enabled if is_enabled
repository.gitaly_blob_client.batch_lfs_pointers(blob_ids) repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
else else
blob_ids.lazy blob_ids.lazy
.select { |sha| possible_lfs_blob?(repository, sha) } .select { |sha| possible_lfs_blob?(repository, sha) }
......
# 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
if log_using_shell?(options)
log_by_shell(sha, options) log_by_shell(sha, options)
else
log_by_walk(sha, options)
end
end end
def count_commits(options) def count_commits(options)
...@@ -993,6 +997,18 @@ module Gitlab ...@@ -993,6 +997,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
...@@ -1172,6 +1188,19 @@ module Gitlab ...@@ -1172,6 +1188,19 @@ module Gitlab
end end
end end
def create_from_bundle(bundle_path)
gitaly_migrate(:create_repo_from_bundle) do |is_enabled|
if is_enabled
gitaly_repository_client.create_from_bundle(bundle_path)
else
run_git!(%W(clone --bare -- #{bundle_path} #{path}), chdir: nil)
self.class.create_hooks(path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
end
end
true
end
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled| gitaly_migrate(:rebase) do |is_enabled|
if is_enabled if is_enabled
...@@ -1564,6 +1593,27 @@ module Gitlab ...@@ -1564,6 +1593,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
......
...@@ -34,6 +34,8 @@ module Gitlab ...@@ -34,6 +34,8 @@ module Gitlab
end end
def batch_lfs_pointers(blob_ids) def batch_lfs_pointers(blob_ids)
return [] if blob_ids.empty?
request = Gitaly::GetLFSPointersRequest.new( request = Gitaly::GetLFSPointersRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
blob_ids: blob_ids blob_ids: blob_ids
......
...@@ -133,11 +133,11 @@ module Gitlab ...@@ -133,11 +133,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
...@@ -3,6 +3,8 @@ module Gitlab ...@@ -3,6 +3,8 @@ module Gitlab
class RepositoryService class RepositoryService
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
MAX_MSG_SIZE = 128.kilobytes.freeze
def initialize(repository) def initialize(repository)
@repository = repository @repository = repository
@gitaly_repo = repository.gitaly_repository @gitaly_repo = repository.gitaly_repository
...@@ -178,6 +180,29 @@ module Gitlab ...@@ -178,6 +180,29 @@ module Gitlab
end end
end end
end end
def create_from_bundle(bundle_path)
request = Gitaly::CreateRepositoryFromBundleRequest.new(repository: @gitaly_repo)
enum = Enumerator.new do |y|
File.open(bundle_path, 'rb') do |f|
while data = f.read(MAX_MSG_SIZE)
request.data = data
y.yield request
request = Gitaly::CreateRepositoryFromBundleRequest.new
end
end
end
GitalyClient.call(
@storage,
:repository_service,
:create_repository_from_bundle,
enum,
timeout: GitalyClient.default_timeout
)
end
end end
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
...@@ -11,11 +11,6 @@ module Gitlab ...@@ -11,11 +11,6 @@ module Gitlab
untar_with_options(archive: archive, dir: dir, options: 'zxf') untar_with_options(archive: archive, dir: dir, options: 'zxf')
end end
def git_clone_bundle(repo_path:, bundle_path:)
execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path}))
Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
end
def mkdir_p(path) def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path) FileUtils.chmod(DEFAULT_MODE, path)
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
def restore def restore
return true unless File.exist?(@path_to_bundle) return true unless File.exist?(@path_to_bundle)
git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle) @project.repository.create_from_bundle(@path_to_bundle)
rescue => e rescue => e
@shared.error(e) @shared.error(e)
false false
......
...@@ -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
...@@ -28,6 +29,7 @@ module QA ...@@ -28,6 +29,7 @@ module QA
autoload :Group, 'qa/factory/resource/group' autoload :Group, 'qa/factory/resource/group'
autoload :Project, 'qa/factory/resource/project' autoload :Project, 'qa/factory/resource/project'
autoload :DeployKey, 'qa/factory/resource/deploy_key' autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner' autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
end end
...@@ -118,6 +120,7 @@ module QA ...@@ -118,6 +120,7 @@ module QA
autoload :Repository, 'qa/page/project/settings/repository' autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd' autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners' autoload :Runners, 'qa/page/project/settings/runners'
end end
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'
......
module QA
module Factory
module Resource
class SecretVariable < Factory::Base
attr_accessor :key, :value
product :key do
Page::Project::Settings::CICD.act do
expand_secret_variables(&:variable_key)
end
end
product :value do
Page::Project::Settings::CICD.act do
expand_secret_variables(&:variable_value)
end
end
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-secret-variables'
project.description = 'project for adding secret variable test'
end
def fabricate!
project.visit!
Page::Menu::Side.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |setting|
setting.expand_secret_variables do |page|
page.fill_variable_key(key)
page.fill_variable_value(value)
page.add_variable
end
end
end
end
end
end
end
...@@ -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
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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