Commit 36838a84 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into '3867-port-to-ce'

# Conflicts:
#   db/schema.rb
parents a5bb17ff c63af942
......@@ -738,8 +738,9 @@ cache gems:
gitlab_git_test:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
variables:
SETUP_DB: "false"
before_script: []
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'MRWidgetAuthor',
props: {
author: { type: Object, required: true },
showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false },
},
directives: {
tooltip,
},
template: `
<a
:href="author.webUrl || author.web_url"
class="author-link inline"
:v-tooltip="showAuthorTooltip"
:title="author.name">
<img
:src="author.avatarUrl || author.avatar_url"
class="avatar avatar-inline s16" />
<span
v-if="showAuthorName"
class="author">{{author.name}}
</span>
</a>
`,
};
<script>
export default {
name: 'MRWidgetAuthor',
props: {
author: {
type: Object,
required: true,
},
},
computed: {
authorUrl() {
return this.author.webUrl || this.author.web_url;
},
avatarUrl() {
return this.author.avatarUrl || this.author.avatar_url;
},
},
};
</script>
<template>
<a
:href="authorUrl"
class="author-link inline"
>
<img
:src="avatarUrl"
class="avatar avatar-inline s16"
/>
<span class="author">
{{ author.name }}
</span>
</a>
</template>
import MRWidgetAuthor from './mr_widget_author';
export default {
name: 'MRWidgetAuthorTime',
props: {
actionText: { type: String, required: true },
author: { type: Object, required: true },
dateTitle: { type: String, required: true },
dateReadable: { type: String, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
template: `
<h4 class="js-mr-widget-author">
{{actionText}}
<mr-widget-author :author="author" />
<time
:title="dateTitle"
data-toggle="tooltip"
data-placement="top"
data-container="body">
{{dateReadable}}
</time>
</h4>
`,
};
<script>
import mrWidgetAuthor from './mr_widget_author.vue';
export default {
name: 'MRWidgetAuthorTime',
components: {
mrWidgetAuthor,
},
props: {
actionText: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
dateTitle: {
type: String,
required: true,
},
dateReadable: {
type: String,
required: true,
},
},
};
</script>
<template>
<h4 class="js-mr-widget-author">
{{ actionText }}
<mr-widget-author :author="author" />
<time
:title="dateTitle"
data-toggle="tooltip"
data-placement="top"
data-container="body"
>
{{ dateReadable }}
</time>
</h4>
</template>
import tooltip from '../../vue_shared/directives/tooltip';
import { pluralize } from '../../lib/utils/text_utility';
import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetHeader',
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
components: {
icon,
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
template: `
<div class="mr-source-target">
<div class="normal">
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
v-html="mr.sourceBranchLink"></span>
<button
v-tooltip
class="btn btn-transparent btn-clipboard"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
:class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
<div v-if="mr.isOpen">
<a
href="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm inline">
Check out branch
</a>
<span class="dropdown prepend-left-10">
<a
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
<icon
name="download">
</icon>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
:href="mr.emailPatchesPath"
download>
Email patches
</a>
</li>
<li>
<a
:href="mr.plainDiffPath"
download>
Plain diff
</a>
</li>
</ul>
</span>
</div>
</div>
`,
};
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script>
<template>
<div class="mr-source-target">
<div class="normal">
<strong>
{{ s__("mrWidget|Request to merge") }}
<span
class="label-branch js-source-branch"
:class="{ 'label-truncated': isSourceBranchLong }"
:title="isSourceBranchLong ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isSourceBranchLong"
v-html="mr.sourceBranchLink"
>
</span>
<clipboard-button
:text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')"
/>
{{ s__("mrWidget|into") }}
<span
class="label-branch"
:v-tooltip="isTargetBranchLong"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }"
:title="isTargetBranchLong ? mr.targetBranch : ''"
data-placement="bottom"
>
<a
:href="mr.targetBranchTreePath"
class="js-target-branch"
>
{{ mr.targetBranch }}
</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count"
>
(<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
</span>
</div>
<div v-if="mr.isOpen">
<button
data-target="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm btn-default inline js-check-out-branch"
type="button"
>
{{ s__("mrWidget|Check out branch") }}
</button>
<span class="dropdown prepend-left-10">
<button
type="button"
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
aria-haspopup="true"
aria-expanded="false"
>
<icon name="download" />
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
class="js-download-email-patches"
:href="mr.emailPatchesPath"
download
>
{{ s__("mrWidget|Email patches") }}
</a>
</li>
<li>
<a
class="js-download-plain-diff"
:href="mr.plainDiffPath"
download
>
{{ s__("mrWidget|Plain diff") }}
</a>
</li>
</ul>
</span>
</div>
</div>
</template>
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: { type: String, required: false, default: '' },
},
template: `
<section class="mr-widget-help">
<template
v-if="missingBranch">
If the {{missingBranch}} branch exists in your local repository, you
</template>
<template v-else>
You
</template>
can merge this merge request manually using the
<a
data-toggle="modal"
href="#modal_merge_info">
command line
</a>
</section>
`,
};
<script>
import { sprintf, s__ } from '~/locale';
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: {
type: String,
required: false,
default: '',
},
},
computed: {
missingBranchInfo() {
return sprintf(
s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'),
{ branch: this.missingBranch },
);
},
},
};
</script>
<template>
<section class="mr-widget-help">
<template v-if="missingBranch">
{{ missingBranchInfo }}
</template>
<template v-else>
{{ s__("mrWidget|You can merge this merge request manually using the") }}
</template>
<button
type="button"
class="btn-link btn-blank js-open-modal-help"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ s__("mrWidget|command line") }}
</button>
</section>
</template>
<script>
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
......
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetAuthor from '../../components/mr_widget_author';
import mrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
......
......@@ -3,7 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
......
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
......
......@@ -11,8 +11,8 @@
export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
......
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
export default {
props: {
inputId: {
type: String,
required: true,
},
confirmationKey: {
type: String,
required: true,
},
confirmationValue: {
type: String,
required: true,
},
shouldEscapeConfirmationValue: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
inputLabel() {
let value = this.confirmationValue;
if (this.shouldEscapeConfirmationValue) {
value = _.escape(value);
}
return sprintf(
__('Type %{value} to confirm:'),
{ value: `<code>${value}</code>` },
false,
);
},
},
methods: {
hasCorrectValue() {
return this.$refs.enteredValue.value === this.confirmationValue;
},
},
};
</script>
<template>
<div>
<label
v-html="inputLabel"
:for="inputId"
>
</label>
<input
:id="inputId"
:name="confirmationKey"
type="text"
ref="enteredValue"
class="form-control"
/>
</div>
</template>
......@@ -410,7 +410,6 @@
width: 298px;
}
@media (max-width: $screen-xs-max) {
display: flex;
width: 100%;
......
......@@ -96,12 +96,12 @@ module ApplicationSettingsHelper
]
end
def repository_storages_options_for_select
def repository_storages_options_for_select(selected)
options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name]
end
options_for_select(options, @application_setting.repository_storages)
options_for_select(options, selected)
end
def sidekiq_queue_options_for_select
......
......@@ -10,6 +10,10 @@ module Ci
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
condition(:non_owner_of_schedule) do
!pipeline_schedule.owned_by?(@user)
end
rule { can?(:developer_access) }.policy do
enable :play_pipeline_schedule
end
......@@ -19,6 +23,10 @@ module Ci
enable :admin_pipeline_schedule
end
rule { can?(:master_access) & non_owner_of_schedule }.policy do
enable :take_ownership_pipeline_schedule
end
rule { protected_ref }.prevent :play_pipeline_schedule
end
end
......@@ -537,7 +537,8 @@
.form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
.col-sm-10
= f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control'
= f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
{include_hidden: false}, multiple: true, class: 'form-control'
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
......
......@@ -15,10 +15,10 @@
%span.text
Checking branch availability…
.btn-group.available.hide
%button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
......
......@@ -29,9 +29,10 @@
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do
= icon('play')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
- if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
= icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
......
---
title: Hide pipeline schedule take ownership for current owner
merge_request: 12986
author:
type: fixed
---
title: Add confirmation-input component
merge_request: 16816
author:
type: other
---
title: Improve issue note dropdown and mr button
merge_request: 16758
author: George Tsiolis
type: changed
---
title: Finish any remaining jobs for issues.closed_at
merge_request:
author:
type: other
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include EachBatch
end
def up
Gitlab::BackgroundMigration.steal('CopyColumn')
Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
# It's possible the cleanup job was killed which means we need to manually
# migrate any remaining rows.
migrate_remaining_rows if migrate_column_type?
end
def down
end
def migrate_remaining_rows
Issue.where('closed_at_for_type_change IS NULL AND closed_at IS NOT NULL').each_batch do |batch|
batch.update_all('closed_at_for_type_change = closed_at')
end
cleanup_concurrent_column_type_change(:issues, :closed_at)
end
def migrate_column_type?
# Some environments may have already executed the previous version of this
# migration, thus we don't need to migrate those environments again.
column_for('issues', 'closed_at').type == :datetime # rubocop:disable Migration/Datetime
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180119135717) do
ActiveRecord::Schema.define(version: 20180201145907) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -8,23 +8,13 @@ comments: false
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development!
GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans:
GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans.
- **GitLab Community Edition (CE)** is an [open source product](https://gitlab.com/gitlab-org/gitlab-ce/),
self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com.
- **GitLab Enterprise Edition (EE)** is an [open-core product](https://gitlab.com/gitlab-org/gitlab-ee/),
self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**.
- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings).
With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
> **GitLab EE** contains all features available in **GitLab CE**,
plus premium features available in each version: **Enterprise Edition Starter**
(**EES**), **Enterprise Edition Premium** (**EEP**), and **Enterprise Edition Ultimate**
(**EEU**). Everything available in **EES** is also available in **EEP**. Every feature
available in **EEP** is also available in **EEU**.
GitLab.com is our SaaS offering. It's hosted, managed, and administered by GitLab, with [free and paid plans](https://about.gitlab.com/gitlab-com/) for individuals and teams: Free, Bronze, Silver, and Gold.
----
Shortcuts to GitLab's most visited docs:
## Shortcuts to GitLab's most visited docs
| [GitLab CI/CD](ci/README.md) | Other |
| :----- | :----- |
......@@ -134,14 +124,8 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
## Administrator documentation
[Administration documentation](administration/index.md) applies to admin users of GitLab
self-hosted instances:
- GitLab Community Edition
- GitLab [Enterprise Editions](https://about.gitlab.com/gitlab-ee/)
- Enterprise Edition Starter (EES)
- Enterprise Edition Premium (EEP)
- Enterprise Edition Ultimate (EEU)
[Administration documentation](administration/index.md) applies to admin users of [GitLab
self-hosted instances](#self-hosted-gitlab): Libre, Starter, Premium, Ultimate.
Learn how to install, configure, update, upgrade, integrate, and maintain your own instance.
Regular users don't have access to GitLab administration tools and settings.
......
......@@ -31,7 +31,7 @@ The default configuration can always be found in the [values.yaml](https://gitla
In order for GitLab Runner to function, your config file **must** specify the following:
- `gitlabURL` - the GitLab Server URL (with protocol) to register the runner against
- `gitlabUrl` - the GitLab Server URL (with protocol) to register the runner against
- `runnerRegistrationToken` - The Registration Token for adding new Runners to the GitLab Server. This must be
retrieved from your GitLab Instance. See the [GitLab Runner Documentation](../../ci/runners/README.md#creating-and-registering-a-runner) for more information.
......@@ -47,7 +47,7 @@ Here is a snippet of the important settings:
## The GitLab Server URL (with protocol) that want to register the runner against
## ref: https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-register
##
gitlabURL: http://gitlab.your-domain.com/
gitlabUrl: http://gitlab.your-domain.com/
## The Registration Token for adding new Runners to the GitLab Server. This must
## be retreived from your GitLab Instance.
......
......@@ -6,6 +6,7 @@ module Gitlab
CommandError = Class.new(StandardError)
CommitError = Class.new(StandardError)
OSError = Class.new(StandardError)
class << self
include Gitlab::EncodingHelper
......
......@@ -1306,7 +1306,15 @@ module Gitlab
# rubocop:enable Metrics/ParameterLists
def write_config(full_path:)
rugged.config['gitlab.fullpath'] = full_path if full_path.present?
return unless full_path.present?
gitaly_migrate(:write_config) do |is_enabled|
if is_enabled
gitaly_repository_client.write_config(full_path: full_path)
else
rugged_write_config(full_path: full_path)
end
end
end
def gitaly_repository
......@@ -1446,6 +1454,10 @@ module Gitlab
end
end
def rugged_write_config(full_path:)
rugged.config['gitlab.fullpath'] = full_path
end
def shell_write_ref(ref_path, ref, old_ref)
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
......@@ -1507,7 +1519,7 @@ module Gitlab
if sparse_checkout_files
# Create worktree without checking out
run_git!(base_args + ['--no-checkout', worktree_path], env: env)
worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path)
worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path).chomp
configure_sparse_checkout(worktree_git_path, sparse_checkout_files)
......
......@@ -219,6 +219,19 @@ module Gitlab
true
end
def write_config(full_path:)
request = Gitaly::WriteConfigRequest.new(repository: @gitaly_repo, full_path: full_path)
response = GitalyClient.call(
@storage,
:repository_service,
:write_config,
request,
timeout: GitalyClient.fast_timeout
)
raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
end
end
end
end
......@@ -4,6 +4,8 @@ module QA
class Dropzone
attr_reader :page, :container
# page - A QA::Page::Base object
# container - CSS selector of the comment textarea's container
def initialize(page, container)
@page = page
@container = container
......
......@@ -27,7 +27,7 @@ module QA
fill_in(with: text, name: 'note[note]')
unless attachment.nil?
QA::Page::Component::Dropzone.new(page, '.new-note')
QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
......
......@@ -84,7 +84,7 @@ module QA
config.javascript_driver = :chrome
config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp'
config.save_path = File.expand_path('../../tmp', __dir__)
end
end
......
......@@ -61,7 +61,7 @@ describe 'Merge request > User selects branches for new MR', :js do
fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Submit merge request"
click_link "Check out branch"
click_button "Check out branch"
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
......
import Vue from 'vue';
import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
const author = {
webUrl: 'http://foo.bar',
avatarUrl: 'http://gravatar.com/foo',
name: 'fatihacet',
};
const createComponent = () => {
const Component = Vue.extend(authorComponent);
return new Component({
el: document.createElement('div'),
propsData: { author },
});
};
import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
describe('props', () => {
it('should have props', () => {
const authorProp = authorComponent.props.author;
let vm;
beforeEach(() => {
const Component = Vue.extend(authorComponent);
vm = mountComponent(Component, {
author: {
name: 'Administrator',
username: 'root',
webUrl: 'http://localhost:3000/root',
avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
expect(authorProp).toBeDefined();
expect(authorProp.type instanceof Object).toBeTruthy();
expect(authorProp.required).toBeTruthy();
});
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
afterEach(() => {
vm.$destroy();
});
expect(el.tagName).toEqual('A');
expect(el.getAttribute('href')).toEqual(author.webUrl);
expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
expect(el.querySelector('.author').innerText.trim()).toEqual(author.name);
it('renders link with the author web url', () => {
expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root');
});
it('renders image with avatar url', () => {
expect(
vm.$el.querySelector('img').getAttribute('src'),
).toEqual('http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon');
});
it('renders author name', () => {
expect(vm.$el.textContent.trim()).toEqual('Administrator');
});
});
import Vue from 'vue';
import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
const props = {
describe('MRWidgetAuthorTime', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(authorTimeComponent);
vm = mountComponent(Component, {
actionText: 'Merged by',
author: {
webUrl: 'http://foo.bar',
avatarUrl: 'http://gravatar.com/foo',
name: 'fatihacet',
name: 'Administrator',
username: 'root',
webUrl: 'http://localhost:3000/root',
avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
dateTitle: '2017-03-23T23:02:00.807Z',
dateReadable: '12 hours ago',
};
const createComponent = () => {
const Component = Vue.extend(authorTimeComponent);
return new Component({
el: document.createElement('div'),
propsData: props,
});
};
describe('MRWidgetAuthorTime', () => {
describe('props', () => {
it('should have props', () => {
const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
const ActionTextClass = actionText.type;
const DateTitleClass = dateTitle.type;
const DateReadableClass = dateReadable.type;
expect(new ActionTextClass() instanceof String).toBeTruthy();
expect(actionText.required).toBeTruthy();
expect(author.type instanceof Object).toBeTruthy();
expect(author.required).toBeTruthy();
expect(new DateTitleClass() instanceof String).toBeTruthy();
expect(dateTitle.required).toBeTruthy();
expect(new DateReadableClass() instanceof String).toBeTruthy();
expect(dateReadable.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components', () => {
expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
it('renders provided action text', () => {
expect(vm.$el.textContent).toContain('Merged by');
});
expect(el.tagName).toEqual('H4');
expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
expect(el.querySelector('time').innerText).toContain(props.dateReadable);
expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
it('renders author', () => {
expect(vm.$el.textContent).toContain('Administrator');
});
it('renders provided time', () => {
expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z');
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago');
});
});
import Vue from 'vue';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
const createComponent = (mr) => {
const Component = Vue.extend(headerComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetHeader', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = headerComponent.props;
let vm;
let Component;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
beforeEach(() => {
Component = Vue.extend(headerComponent);
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
let vm;
beforeEach(() => {
vm = createComponent({
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '/foo/bar/mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
expect(vm.shouldShowCommitsBehindText).toEqual(true);
});
it('returns false where there are no divergedComits count', () => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
expect(vm.shouldShowCommitsBehindText).toEqual(false);
});
});
it('shouldShowCommitsBehindText', () => {
expect(vm.shouldShowCommitsBehindText).toBeTruthy();
describe('commitsText', () => {
it('returns singular when there is one commit', () => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 1,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
vm.mr.divergedCommitsCount = 0;
expect(vm.shouldShowCommitsBehindText).toBeFalsy();
expect(vm.commitsText).toEqual('1 commit behind');
});
it('commitsText', () => {
expect(vm.commitsText).toEqual('commits');
it('returns plural when there is more than one commit', () => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 2,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
targetBranch: 'master',
} });
vm.mr.divergedCommitsCount = 1;
expect(vm.commitsText).toEqual('commit');
expect(vm.commitsText).toEqual('2 commits behind');
});
});
});
describe('template', () => {
let vm;
let el;
let mr;
const sourceBranchPath = '/foo/bar/mr-widget-refactor';
describe('common elements', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('renders source branch link', () => {
expect(
vm.$el.querySelector('.js-source-branch').innerHTML,
).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
});
it('renders clipboard button', () => {
expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null);
});
it('renders target branch', () => {
expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master');
});
});
describe('with an open merge request', () => {
afterEach(() => {
vm.$destroy();
});
beforeEach(() => {
mr = {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`,
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
......@@ -63,59 +112,109 @@ describe('MRWidgetHeader', () => {
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
};
} });
});
it('renders checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch');
vm = createComponent(mr);
el = vm.$el;
expect(button.textContent.trim()).toEqual('Check out branch');
expect(button.getAttribute('data-target')).toEqual('#modal_merge_info');
expect(button.getAttribute('data-toggle')).toEqual('modal');
});
it('should render template elements correctly', () => {
expect(el.classList.contains('mr-source-target')).toBeTruthy();
const sourceBranchLink = el.querySelectorAll('.label-branch')[0];
const targetBranchLink = el.querySelectorAll('.label-branch')[1];
const commitsCount = el.querySelector('.diverged-commits-count');
it('renders download dropdown with links', () => {
expect(
vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
).toEqual('Email patches');
expect(sourceBranchLink.textContent).toContain(mr.sourceBranch);
expect(targetBranchLink.textContent).toContain(mr.targetBranch);
expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath);
expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath);
expect(commitsCount.textContent).toContain('12 commits behind');
expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(
vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
).toEqual('/mr/email-patches');
expect(el.textContent).toContain('Check out branch');
expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
expect(
vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
).toEqual('Plain diff');
expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBeNull();
expect(
vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
).toEqual('/mr/plainDiffPath');
});
});
it('should not have right action links if the MR state is not open', (done) => {
vm.mr.isOpen = false;
Vue.nextTick(() => {
expect(el.textContent).not.toContain('Check out branch');
expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
done();
describe('with a closed merge request', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: false,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('does not render checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch');
expect(button).toEqual(null);
});
it('should not render diverged commits count if the MR has no diverged commits', (done) => {
vm.mr.divergedCommitsCount = null;
Vue.nextTick(() => {
expect(el.textContent).not.toContain('commits behind');
expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
done();
it('does not render download dropdown with links', () => {
expect(
vm.$el.querySelector('.js-download-email-patches'),
).toEqual(null);
expect(
vm.$el.querySelector('.js-download-plain-diff'),
).toEqual(null);
});
});
it('should disable check out branch button if source branch has been removed', (done) => {
vm.mr.sourceBranchRemoved = true;
describe('without diverged commits', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('does not render diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null);
});
});
Vue.nextTick()
.then(() => {
expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBe('disabled');
done();
})
.catch(done.fail);
describe('with diverged commits', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
sourceBranchRemoved: false,
targetBranchPath: 'foo/bar/commits-path',
targetBranchTreePath: 'foo/bar/tree/path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
} });
});
it('renders diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
});
});
});
});
import Vue from 'vue';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
const props = {
missingBranch: 'this-is-not-the-branch-you-are-looking-for',
};
const text = `If the ${props.missingBranch} branch exists in your local repository`;
describe('MRWidgetMergeHelp', () => {
let vm;
let Component;
const createComponent = () => {
const Component = Vue.extend(mergeHelpComponent);
return new Component({
el: document.createElement('div'),
propsData: props,
beforeEach(() => {
Component = Vue.extend(mergeHelpComponent);
});
};
describe('MRWidgetMergeHelp', () => {
describe('props', () => {
it('should have props', () => {
const { missingBranch } = mergeHelpComponent.props;
const MissingBranchTypeClass = missingBranch.type;
afterEach(() => {
vm.$destroy();
});
expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
expect(missingBranch.required).toBeFalsy();
expect(missingBranch.default).toEqual('');
describe('with missing branch', () => {
beforeEach(() => {
vm = mountComponent(Component, {
missingBranch: 'this-is-not-the-branch-you-are-looking-for',
});
});
describe('template', () => {
let vm;
let el;
it('renders missing branch information', () => {
expect(
vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
).toEqual(
'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line',
);
});
beforeEach(() => {
vm = createComponent();
el = vm.$el;
it('renders button to open help modal', () => {
expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
});
});
it('should have the correct elements', () => {
expect(el.classList.contains('mr-widget-help')).toBeTruthy();
expect(el.textContent).toContain(text);
describe('without missing branch', () => {
beforeEach(() => {
vm = mountComponent(Component);
});
it('should not show missing branch name if missingBranch props is not provided', (done) => {
vm.missingBranch = null;
Vue.nextTick(() => {
expect(el.textContent).not.toContain(text);
done();
it('renders information about how to merge manually', () => {
expect(
vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
).toEqual(
'You can merge this merge request manually using the command line',
);
});
it('renders element to open a modal', () => {
expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
});
});
});
import Vue from 'vue';
import confirmationInput from '~/vue_shared/components/confirmation_input.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Confirmation input component', () => {
const Component = Vue.extend(confirmationInput);
const props = {
inputId: 'dummy-id',
confirmationKey: 'confirmation-key',
confirmationValue: 'confirmation-value',
};
let vm;
afterEach(() => {
vm.$destroy();
});
describe('props', () => {
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('sets id of the input field to inputId', () => {
expect(vm.$refs.enteredValue.id).toBe(props.inputId);
});
it('sets name of the input field to confirmationKey', () => {
expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey);
});
});
describe('computed', () => {
describe('inputLabel', () => {
it('escapes confirmationValue by default', () => {
vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' });
expect(vm.inputLabel).toBe('Type <code>n&lt;e&gt;&lt;/e&gt;ds escap&quot;ng</code> to confirm:');
});
it('does not escape confirmationValue if escapeValue is false', () => {
vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false });
expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:');
});
});
});
describe('methods', () => {
describe('hasCorrectValue', () => {
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('returns false if entered value is incorrect', () => {
vm.$refs.enteredValue.value = 'incorrect';
expect(vm.hasCorrectValue()).toBe(false);
});
it('returns true if entered value is correct', () => {
vm.$refs.enteredValue.value = props.confirmationValue;
expect(vm.hasCorrectValue()).toBe(true);
});
});
});
});
......@@ -20,6 +20,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:storage_path) { TestEnv.repos_path }
let(:user) { build(:user) }
describe '.create_hooks' do
let(:repo_path) { File.join(storage_path, 'hook-test.git') }
......@@ -693,7 +694,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#remote_tags' do
let(:remote_name) { 'upstream' }
let(:target_commit_id) { SeedRepo::Commit::ID }
let(:user) { create(:user) }
let(:tag_name) { 'v0.0.1' }
let(:tag_message) { 'My tag' }
let(:remote_repository) do
......@@ -1711,7 +1711,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples "user deleting a branch" do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
let(:user) { create(:user) }
let(:branch_name) { "to-be-deleted-soon" }
before do
......@@ -1752,12 +1751,49 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#write_config' do
before do
repository.rugged.config["gitlab.fullpath"] = repository.path
end
shared_examples 'writing repo config' do
context 'is given a path' do
it 'writes it to disk' do
repository.write_config(full_path: "not-the/real-path.git")
config = File.read(File.join(repository.path, "config"))
expect(config).to include("[gitlab]")
expect(config).to include("fullpath = not-the/real-path.git")
end
end
context 'it is given an empty path' do
it 'does not write it to disk' do
repository.write_config(full_path: "")
config = File.read(File.join(repository.path, "config"))
expect(config).to include("[gitlab]")
expect(config).to include("fullpath = #{repository.path}")
end
end
end
context "when gitaly_write_config is enabled" do
it_behaves_like "writing repo config"
end
context "when gitaly_write_config is disabled", :disable_gitaly do
it_behaves_like "writing repo config"
end
end
describe '#merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
let(:user) { build(:user) }
let(:target_branch) { 'test-merge-target-branch' }
before do
......@@ -1810,7 +1846,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:user) { build(:user) }
let(:target_branch) { 'test-ff-target-branch' }
before do
......@@ -2129,6 +2164,47 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
end
end
describe '#squash' do
let(:squash_id) { '1' }
let(:branch_name) { 'fix' }
let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }
subject do
opts = {
branch: branch_name,
start_sha: start_sha,
end_sha: end_sha,
author: user,
message: 'Squash commit message'
}
repository.squash(user, squash_id, opts)
end
context 'sparse checkout' do
let(:expected_files) { %w(files files/js files/js/application.js) }
before do
allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
m.call(*args) do
worktree_path = args[0]
files_pattern = File.join(worktree_path, '**', '*')
expected = expected_files.map do |path|
File.expand_path(path, worktree_path)
end
expect(Dir[files_pattern]).to eq(expected)
end
end
end
it 'checkouts only the files in the diff' do
subject
end
end
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
......
......@@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do
expect(policy).to be_allowed :admin_pipeline_schedule
end
end
describe 'rules for non-owner of schedule' do
let(:owner) { create(:user) }
before do
project.add_master(owner)
project.add_master(user)
pipeline_schedule.update(owner: owner)
end
it 'includes abilities to take ownership' do
expect(policy).to be_allowed :take_ownership_pipeline_schedule
end
end
end
end
require 'spec_helper'
describe 'projects/pipeline_schedules/_pipeline_schedule' do
let(:owner) { create(:user) }
let(:master) { create(:user) }
let(:project) { create(:project) }
let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
before do
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule)
allow(view).to receive(:can?).and_return(true)
end
context 'taking ownership of schedule' do
context 'when non-owner is signed in' do
let(:user) { master }
before do
allow(view).to receive(:can?).with(master, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true)
end
it 'non-owner can take ownership of pipeline' do
render
expect(rendered).to have_link('Take ownership')
end
end
context 'when owner is signed in' do
let(:user) { owner }
before do
allow(view).to receive(:can?).with(owner, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(false)
end
it 'owner cannot take ownership of pipeline' do
render
expect(rendered).not_to have_link('Take ownership')
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