Commit 1a2baf98 authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Merge branch 'master' into 4310-security-reports

* master: (73 commits)
  Add a lint check to restrict use of Rugged, EE version
  Fix EE offenses to the RSpec/SingleLineHook cop
  Update qa.rb
  Revert "Merge branch 'osw-updates-merge-status-on-api-actions' into 'master'"
  Use a fixed remote name for Geo mirrors
  Use flash message instead of regular text block
  Update common.rb
  Update gitlab-styles and re-enable the RSpec/SingleLineHook cop again
  Move EE-specific migrations under ee/db/
  Fix the EE assets paths and precompile config
  Add modal for deleting a milestone
  Make Gitlab::Git::Repository#run_git private
  Resolve conflicts in app/assets/javascripts/dispatcher.js
  Resolve conflicts in app/assets/javascripts/dispatcher.js
  Resolve conflicts in lib/gitlab/git/repository.rb
  Resolve conflict in spec/services/issues/move_service_spec.rb
  Resolve conflict in features/support/db_cleaner.rb
  Resolve conflict in spec/support/db_cleaner.rb
  Resolve conflict with .gitlab-ci.yml
  Update secret_values to support dynamic elements within parent
  ...
parents f7fc4d04 595a616c
......@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
key: "ruby-235-with-yarn"
key: "ruby-2.3.6-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
......
......@@ -13,11 +13,24 @@ AllCops:
- 'db/*'
- 'db/fixtures/**/*'
- 'db/geo/*'
- 'ee/db/geo/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
# This cop checks whether some constant value isn't a
# mutable literal (e.g. array or hash).
Style/MutableConstant:
Enabled: true
Exclude:
- 'db/migrate/**/*'
- 'db/post_migrate/**/*'
- 'db/geo/migrate/**/*'
- 'ee/db/migrate/**/*'
- 'ee/db/post_migrate/**/*'
- 'ee/db/geo/migrate/**/*'
# Gitlab ###################################################################
Gitlab/ModuleWithInstanceVariables:
......
......@@ -342,10 +342,6 @@ RSpec/SharedContext:
Exclude:
- 'spec/features/admin/admin_groups_spec.rb'
# Offense count: 90
RSpec/SingleLineHook:
Enabled: false
# Offense count: 5
RSpec/VoidExpect:
Exclude:
......
......@@ -422,7 +422,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.74.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.76.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.74.0)
gitaly-proto (0.76.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3)
gitlab-license (1.0.0)
gitlab-markup (1.6.3)
gitlab-styles (2.3.0)
gitlab-styles (2.3.1)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
......@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.74.0)
gitaly-proto (~> 0.76.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -2,18 +2,19 @@ import { n__ } from '../locale';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
export default class SecretValues {
constructor(container) {
constructor({
container,
valueSelector = '.js-secret-value',
placeholderSelector = '.js-secret-value-placeholder',
}) {
this.container = container;
this.valueSelector = valueSelector;
this.placeholderSelector = placeholderSelector;
}
init() {
this.values = this.container.querySelectorAll('.js-secret-value');
this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder');
this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
this.revealText = n__('Reveal value', 'Reveal values', this.values.length);
this.hideText = n__('Hide value', 'Hide values', this.values.length);
const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
this.updateDom(isRevealed);
......@@ -28,15 +29,17 @@ export default class SecretValues {
}
updateDom(isRevealed) {
this.values.forEach((value) => {
const values = this.container.querySelectorAll(this.valueSelector);
values.forEach((value) => {
value.classList.toggle('hide', !isRevealed);
});
this.placeholders.forEach((placeholder) => {
const placeholders = this.container.querySelectorAll(this.placeholderSelector);
placeholders.forEach((placeholder) => {
placeholder.classList.toggle('hide', isRevealed);
});
this.revealButton.textContent = isRevealed ? this.hideText : this.revealText;
this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length);
this.revealButton.dataset.secretRevealStatus = isRevealed;
}
}
......@@ -132,14 +132,17 @@ $(() => {
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
newIssue.setFetchingState('weight', true);
newIssue.setFetchingState('epic', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.data)
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.setFetchingState('weight', false);
newIssue.setFetchingState('epic', false);
newIssue.updateData({
subscribed: data.subscribed,
weight: data.weight,
epic: data.epic,
});
})
.catch(() => {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import notificationsDropdown from './notifications_dropdown';
import LineHighlighter from './line_highlighter';
import MergeRequest from './merge_request';
import Flash from './flash';
import BlobViewer from './blob/viewer/index';
import GfmAutoComplete from './gfm_auto_complete';
import Star from './star';
import ZenMode from './zen_mode';
import PerformanceBar from './performance_bar';
import initNotes from './init_notes';
import initIssuableSidebar from './init_issuable_sidebar';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
......@@ -117,6 +112,11 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:index':
import('./pages/projects/milestones/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:show':
import('./pages/projects/milestones/show')
.then(callDefault)
......@@ -700,23 +700,12 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
.then(callDefault)
.catch(fail);
break;
case 'show':
new Star();
notificationsDropdown();
break;
case 'wikis':
import('./pages/projects/wikis')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'snippets':
if (path[2] === 'show') {
new ZenMode();
new LineHighlighter();
new BlobViewer();
}
break;
}
break;
}
......@@ -726,7 +715,9 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
}
if (document.querySelector('#peek')) {
new PerformanceBar({ container: '#peek' });
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(fail);
}
};
......
import initMilestonesShow from '~/pages/init_milestones_show';
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
export default initMilestonesShow;
......@@ -3,7 +3,9 @@ import SecretValues from '~/behaviors/secret_values';
export default () => {
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
const secretVariableTableValues = new SecretValues(secretVariableTable);
const secretVariableTableValues = new SecretValues({
container: secretVariableTable,
});
secretVariableTableValues.init();
}
};
<script>
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import modal from '~/vue_shared/components/modal.vue';
import { n__, s__, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
props: {
issueCount: {
type: Number,
required: true,
},
mergeRequestCount: {
type: Number,
required: true,
},
milestoneId: {
type: Number,
required: true,
},
milestoneTitle: {
type: String,
required: true,
},
milestoneUrl: {
type: String,
required: true,
},
},
computed: {
text() {
const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle });
if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf(
s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project.
%{milestoneTitle} is not currently used in any issues or merge requests.`),
{
milestoneTitle,
},
false,
);
}
return sprintf(
s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
Once deleted, it cannot be undone or recovered.`),
{
milestoneTitle,
issuesWithCount: n__('%d issue', '%d issues', this.issueCount),
mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount),
},
false,
);
},
title() {
return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle });
},
},
methods: {
onSubmit() {
eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl);
return axios.delete(this.milestoneUrl)
.then((response) => {
eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true });
// follow the rediect to milestones overview page
redirectTo(response.request.responseURL);
})
.catch((error) => {
eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false });
if (error.response && error.response.status === 404) {
Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle }));
} else {
Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle }));
}
throw error;
});
},
},
};
</script>
<template>
<modal
id="delete-milestone-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="s__('Milestones|Delete milestone')"
@submit="onSubmit">
<template
slot="body"
slot-scope="props">
<p v-html="props.text"></p>
</template>
</modal>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
export default () => {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
button.querySelector('.js-loading-icon').classList.add('hidden');
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
milestoneTitle: button.dataset.milestoneTitle,
milestoneUrl: button.dataset.milestoneUrl,
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
};
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('deleteMilestoneModal.props', modalProps);
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
const button = deleteMilestoneButtons[i];
button.addEventListener('click', onDeleteButtonClick);
}
eventHub.$once('deleteMilestoneModal.mounted', () => {
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
const button = deleteMilestoneButtons[i];
button.removeAttribute('disabled');
}
});
return new Vue({
el: '#delete-milestone-modal',
components: {
deleteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneId: -1,
milestoneTitle: '',
milestoneUrl: '',
issueCount: -1,
mergeRequestCount: -1,
},
};
},
mounted() {
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
eventHub.$emit('deleteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement(deleteMilestoneModal, {
props: this.modalProps,
});
},
});
};
import milestones from '~/pages/milestones/shared';
export default milestones;
import initMilestonesShow from '~/pages/init_milestones_show';
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import milestones from '~/pages/milestones/shared';
export default initMilestonesShow;
export default () => {
initMilestonesShow();
milestones();
};
......@@ -6,13 +6,17 @@ export default function () {
initSettingsPanels();
const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) {
const runnerTokenSecretValue = new SecretValues(runnerToken);
const runnerTokenSecretValue = new SecretValues({
container: runnerToken,
});
runnerTokenSecretValue.init();
}
const secretVariableTable = document.querySelector('.js-secret-variable-table');
if (secretVariableTable) {
const secretVariableTableValues = new SecretValues(secretVariableTable);
const secretVariableTableValues = new SecretValues({
container: secretVariableTable,
});
secretVariableTableValues.init();
}
}
......@@ -5,8 +5,12 @@ import TreeView from '~/tree';
import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
export default () => {
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
new NotificationsForm(); // eslint-disable-line no-new
new UserCallout({ // eslint-disable-line no-new
......
import initNotes from '~/init_notes';
import ZenMode from '~/zen_mode';
import LineHighlighter from '../../../../line_highlighter';
import BlobViewer from '../../../../blob/viewer';
export default function () {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
}
......@@ -9,6 +9,7 @@
.modal-body {
background-color: $modal-body-bg;
line-height: $line-height-base;
min-height: $modal-body-height;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
......
......@@ -155,6 +155,8 @@ class ApplicationController < ActionController::Base
format.html do
render file: Rails.root.join("public", "404"), layout: false, status: "404"
end
# Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file
format.js { render json: '', status: :not_found, content_type: 'application/json' }
format.any { head :not_found }
end
end
......
......@@ -88,7 +88,7 @@ class Projects::MilestonesController < Projects::ApplicationController
Milestones::DestroyService.new(project, current_user).execute(milestone)
respond_to do |format|
format.html { redirect_to namespace_project_milestones_path, status: 302 }
format.html { redirect_to namespace_project_milestones_path, status: 303 }
format.js { head :ok }
end
end
......
......@@ -537,7 +537,7 @@ module Ci
return unless sha
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
rescue GRPC::NotFound, GRPC::Internal
nil
end
......
......@@ -45,14 +45,7 @@ class Deployment < ActiveRecord::Base
def includes_commit?(commit)
return false unless commit
# Before 8.10, deployments didn't have keep-around refs. Any deployment
# created before then could have a `sha` referring to a commit that no
# longer exists in the repository, so just ignore those.
begin
project.repository.ancestor?(commit.id, sha)
rescue Rugged::OdbError
false
end
project.repository.ancestor?(commit.id, sha)
end
def update_merge_request_metrics!
......
......@@ -981,9 +981,9 @@ class Project < ActiveRecord::Base
hooks.hooks_for(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
end
end
SystemHooksService.new.execute_hooks(data, hooks_scope)
SystemHooksService.new.execute_hooks(data, hooks_scope)
end
end
def execute_services(data, hooks_scope = :push_hooks)
......
......@@ -25,6 +25,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
......@@ -172,16 +173,10 @@ class Repository
return []
end
raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
commits =
if is_enabled
find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
else
find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
end
CommitCollection.new(project, commits, ref)
commits = raw_repository.find_commits_by_message(query, ref, path, limit, offset).map do |c|
commit(c)
end
CommitCollection.new(project, commits, ref)
end
def find_branch(name, fresh_repo: true)
......@@ -746,23 +741,6 @@ class Repository
Commit.order_by(collection: commits, order_by: order_by, sort: sort)
end
def refs_contains_sha(ref_type, sha)
args = %W(#{ref_type} --contains #{sha})
names = run_git(args).first
if names.respond_to?(:split)
names = names.split("\n").map(&:strip)
names.each do |name|
name.slice! '* '
end
names
else
[]
end
end
def branch_names_contains(sha)
refs_contains_sha('branch', sha)
end
......@@ -969,25 +947,6 @@ class Repository
end
end
def search_files_by_content(query, ref)
return [] if empty? || query.blank?
offset = 2
args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
run_git(args).first.scrub.split(/^--$/)
end
def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(/^\/*/, ""))
return [] if empty? || safe_query.blank?
args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query})
run_git(args).first.lines.map(&:strip)
end
def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil)
unless remote_name
remote_name = "tmp-#{SecureRandom.hex}"
......@@ -1035,6 +994,18 @@ class Repository
raw_repository.ls_files(actual_ref)
end
def search_files_by_content(query, ref)
return [] if empty? || query.blank?
raw_repository.search_files_by_content(query, ref)
end
def search_files_by_name(query, ref)
return [] if empty?
raw_repository.search_files_by_name(query, ref)
end
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
......@@ -1201,25 +1172,4 @@ class Repository
def rugged_can_be_merged?(their_commit, our_commit)
!rugged.merge_commits(our_commit, their_commit).conflicts?
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref
args = %W(
log #{ref} --pretty=%H --skip #{offset}
--max-count #{limit} --grep=#{query} --regexp-ignore-case
)
args = args.concat(%W(-- #{path})) if path.present?
git_log_results = run_git(args).first.lines
git_log_results.map { |c| commit(c.chomp) }.compact
end
def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
raw_repository
.gitaly_commit_client
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
end
end
......@@ -43,7 +43,6 @@
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
......
......@@ -4,6 +4,7 @@
- branch_label = s_('ChangeTypeActionLabel|Revert in branch')
- revert_merge_request = _('Revert this merge request')
- revert_commit = _('Revert this commit')
- description = s_('ChangeTypeAction|This will create a new commit in order to revert the existing changes.')
- title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit
- when 'cherry-pick'
- label = s_('ChangeTypeAction|Cherry-pick')
......@@ -17,6 +18,8 @@
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= title
.modal-body
- if description
%p.append-bottom-20= description
= form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'start_branch', branch_label, class: 'control-label'
......
......@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
%button.btn.js-settings-toggle.qa-expand-deploy-keys
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
......
......@@ -12,6 +12,8 @@
New milestone
.milestones
#delete-milestone-modal
%ul.content-list
= render @milestones
......
......@@ -35,8 +35,18 @@
- else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
= link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal',
target: '#delete-milestone-modal',
milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title),
milestone_url: project_milestone_path(@project, @milestone),
milestone_issue_count: @milestone.issues.count,
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
#delete-milestone-modal
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
......
......@@ -57,7 +57,6 @@
Titles and Filenames
%span.badge
= @search_results.snippet_titles_count
- else
%li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
......
......@@ -19,6 +19,7 @@
= custom_icon("icon_close", size: 15)
.js-issuable-update
= render "shared/boards/components/sidebar/assignee"
= render "shared/boards/components/sidebar/epic"
= render "shared/boards/components/sidebar/milestone"
= render "shared/boards/components/sidebar/due_date"
= render "shared/boards/components/sidebar/labels"
......
......@@ -56,6 +56,13 @@
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
= link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do
Delete
%button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal',
target: '#delete-milestone-modal',
milestone_id: milestone.id,
milestone_title: markdown_field(milestone, :title),
milestone_url: project_milestone_path(milestone.project, milestone),
milestone_issue_count: milestone.issues.count,
milestone_merge_request_count: milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
......@@ -20,10 +20,7 @@ module RepositoryCheck
# Historically some projects never had their wiki repos initialized;
# this happens on project creation now. Let's initialize an empty repo
# if it is not already there.
begin
project.create_wiki
rescue Rugged::RepositoryError
end
project.create_wiki
git_fsck(project.wiki.repository)
else
......
---
title: 'Geo: Improve replication status. Using pg_stat_wal_receiver'
merge_request:
author:
type: other
---
title: Add Epic information for selected issue in Issue boards sidebar
merge_request: 4104
author:
type: added
---
title: Use a fixed remote name for Geo mirrors
merge_request: 4249
author:
type: fixed
---
title: Add application create API
merge_request: 8160
author: Nicolas Merelli @PNSalocin
---
title: Changes Revert this merge request text
merge_request: 16611
author: Jacopo Beschi @jacopo-beschi
type: changed
---
title: Execute system hooks after-commit when executing project hooks
merge_request:
author:
type: fixed
---
title: Add modal for deleting a milestone
merge_request: 16229
author:
type: other
......@@ -111,17 +111,11 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
# Support legacy unicode file named img emojis, `1F939.png`
config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
# EE specific paths.
config.assets.paths << "ee/app/assets/images"
config.assets.paths << "ee/app/assets/javascripts"
config.assets.paths << "ee/app/assets/stylesheets"
config.assets.paths << "#{config.root}/vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "*.ico"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
......@@ -130,10 +124,23 @@ module Gitlab
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css"
config.assets.precompile << "locale/**/app.js"
## EE-specific assets config START
%w[images javascripts stylesheets].each do |path|
config.assets.paths << "#{config.root}/ee/app/assets/#{path}"
end
# Compile non-JS/CSS assets in the ee/app/assets folder by default
# Mimic sprockets-rails default: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87
LOOSE_EE_APP_ASSETS = lambda do |logical_path, filename|
filename.start_with?(config.root.join("ee/app/assets").to_s) &&
!['.js', '.css', ''].include?(File.extname(logical_path))
end
config.assets.precompile << LOOSE_EE_APP_ASSETS
## EE-specific assets config END
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
......
......@@ -12,3 +12,12 @@ unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
ActiveRecord::Migrator.migrations_paths << path
end
end
migrate_paths = Rails.application.config.paths['db/migrate'].to_a
migrate_paths.each do |migrate_path|
absolute_migrate_path = Pathname.new(migrate_path).realpath(Rails.root)
ee_migrate_path = Rails.root.join('ee/', absolute_migrate_path.relative_path_from(Rails.root))
Rails.application.config.paths['db/migrate'] << ee_migrate_path.to_s
ActiveRecord::Migrator.migrations_paths << ee_migrate_path.to_s
end
# We don't want to ever call Rugged::Repository#fetch_attributes, because it has
# a lot of I/O overhead:
# <https://gitlab.com/gitlab-org/gitlab_git/commit/340e111e040ae847b614d35b4d3173ec48329015>
#
# While we don't do this from within the GitLab source itself, the Linguist gem
# has a dependency on Rugged and uses the gitattributes file when calculating
# repository-wide language statistics:
# <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36>
#
# The options passed by Linguist are those assumed by Gitlab::Git::Attributes
# anyway, and there is no great efficiency gain from just fetching the listed
# attributes with our implementation, so we ignore the additional arguments.
#
module Rugged
class Repository
module UseGitlabGitAttributes
def fetch_attributes(name, *)
attributes.attributes(name)
end
def attributes
@attributes ||= Gitlab::Git::Attributes.new(path)
end
end
prepend UseGitlabGitAttributes
end
end
......@@ -96,7 +96,6 @@ var config = {
test: './test.js',
two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
performance_bar: './performance_bar.js',
webpack_runtime: './webpack.js',
},
......
# Applications API
> [Introduced][ce-8160] in GitLab 10.5
[ce-8160]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8160
## Create a application
Create a application by posting a JSON payload.
User must be admin to do that.
Returns `200` if the request succeeds.
```
POST /applications
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | yes | The name of the application |
| `redirect_uri` | string | yes | The redirect URI of the application |
| `scopes` | string | yes | The scopes of the application |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications
```
Example response:
```json
{
"application_id": "5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737",
"secret": "ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34",
"callback_url": "http://redirect.uri"
}
```
......@@ -111,7 +111,7 @@ future GitLab releases.**
| `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` |
| `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` |
## `.gitlab-ci.yaml` defined variables
## `.gitlab-ci.yml` defined variables
>**Note:**
This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher.
......
......@@ -167,7 +167,7 @@ otherwise it may fail with an encryption error.
Secondary Geo nodes track data about what has been downloaded in a second
PostgreSQL database that is distinct from the production GitLab database.
The database configuration is set in `config/database_geo.yml`.
`db/geo` contains the schema and migrations for this database.
`ee/db/geo` contains the schema and migrations for this database.
To write a migration for the database, use the `GeoMigrationGenerator`:
......
......@@ -13,6 +13,7 @@ module Geo
attr_reader :project
GEO_REMOTE_NAME = 'geo'.freeze
LEASE_TIMEOUT = 8.hours.freeze
LEASE_KEY_PREFIX = 'geo_sync_service'.freeze
RETRY_BEFORE_REDOWNLOAD = 5
......@@ -82,7 +83,9 @@ module Geo
authorization = ::Gitlab::Geo::BaseRequest.new.authorization
header = { "http.#{url}.extraHeader" => "Authorization: #{authorization}" }
repository.with_config(header) { repository.fetch_as_mirror(url, forced: true) }
repository.with_config(header) do
repository.fetch_as_mirror(url, remote_name: GEO_REMOTE_NAME, forced: true)
end
end
def registry
......
......@@ -18,6 +18,10 @@
%strong exact order
they appear.
- unless Gitlab::Database.pg_stat_wal_receiver_supported?
= content_for :flash_message do
.alert.alert-warning WARNING: Please upgrade PostgreSQL to version 9.6 or greater. The status of the replication cannot be determined reliably with the current version.
- if @nodes.any?
#js-geo-nodes{ data: { primary_version: "#{Gitlab::VERSION}", primary_revision: "#{Gitlab::REVISION}", node_details_path: "#{admin_geo_nodes_path}", node_actions_allowed: "#{Gitlab::Database.read_write?}", node_edit_allowed: "#{Gitlab::Geo.license_allows?}" } }
- else
......
- return unless @group&.feature_available?(:epics) || @project&.group&.feature_available?(:epics)
.block.epic
.title
Epic
%span.js-epic-label-loading{ "v-if" => "issue.isFetching && issue.isFetching.epic" }
= icon('spinner spin', class: 'loading-icon')
.value.js-epic-label{ "v-if" => "issue.isFetching && !issue.isFetching.epic" }
%a.bold{ "v-if" => "issue.epic", ":href" => "issue.epic.url" }
{{ issue.epic.title }}
.no-value{ "v-if" => "!issue.epic" }
None
......@@ -12,6 +12,6 @@ class GeoMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
def create_migration_file
set_local_assigns!
validate_file_name!
migration_template @migration_template, "db/geo/migrate/#{file_name}.rb"
migration_template @migration_template, "ee/db/geo/migrate/#{file_name}.rb"
end
end
......@@ -5,7 +5,7 @@ module Gitlab
DATABASE_CONFIG = 'config/database.yml'.freeze
GEO_DATABASE_CONFIG = 'config/database_geo.yml'.freeze
GEO_DB_DIR = 'db/geo'.freeze
GEO_DB_DIR = 'ee/db/geo'.freeze
def method_missing(method_name, *args, &block)
with_geo_db do
......
......@@ -7,7 +7,7 @@ module Gitlab
return '' unless Gitlab::Geo.secondary?
return 'The Geo database configuration file is missing.' unless Gitlab::Geo.geo_database_configured?
return 'The Geo node has a database that is not configured for streaming replication with the primary node.' unless self.database_secondary?
return 'The Geo node does not appear to be replicating data from the primary node.' unless self.db_replication_lag_seconds.present?
return 'The Geo node does not appear to be replicating data from the primary node.' if Gitlab::Database.pg_stat_wal_receiver_supported? && !self.streaming_active?
database_version = self.get_database_version.to_i
migration_version = self.get_migration_version.to_i
......@@ -76,6 +76,18 @@ module Gitlab
lag.present? ? lag.to_i : lag
end
def self.streaming_active?
# Get a streaming status
# This only works for Postgresql 9.6 and greater
status =
ActiveRecord::Base.connection.execute('
SELECT * FROM pg_stat_wal_receiver;')
.first
.fetch('status')
status == 'streaming'
end
end
end
end
......@@ -41,14 +41,14 @@ namespace :geo do
puts "Current version: #{Gitlab::Geo::DatabaseTasks.version}"
end
desc 'Drops and recreates the database from db/geo/schema.rb for the current environment and loads the seeds.'
desc 'Drops and recreates the database from ee/db/geo/schema.rb for the current environment and loads the seeds.'
task reset: [:environment] do
ns['drop'].invoke
ns['create'].invoke
ns['setup'].invoke
end
desc 'Load the seed data from db/geo/seeds.rb'
desc 'Load the seed data from ee/db/geo/seeds.rb'
task seed: [:environment] do
ns['abort_if_pending_migrations'].invoke
......@@ -97,7 +97,7 @@ namespace :geo do
Gitlab::Geo::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
desc 'Create a db/geo/schema.rb file that is portable against any DB supported by AR'
desc 'Create a ee/db/geo/schema.rb file that is portable against any DB supported by AR'
task dump: [:environment] do
Gitlab::Geo::DatabaseTasks::Schema.dump
......@@ -219,6 +219,12 @@ namespace :geo do
puts GeoNode.current_node_url
puts '-----------------------------------------------------'.color(:yellow)
unless Gitlab::Database.pg_stat_wal_receiver_supported?
puts
puts 'WARNING: Please upgrade PostgreSQL to version 9.6 or greater. The status of the replication cannot be determined reliably with the current version.'.color(:red)
puts
end
print 'GitLab version: '.rjust(COLUMN_WIDTH)
puts Gitlab::VERSION
......
......@@ -18,12 +18,15 @@ Feature: Project Issues Milestones
Given I click link "New Milestone"
And I submit new milestone "v2.3"
Then I should see milestone "v2.3"
Given I click link to remove milestone
Given I click button to remove milestone
And I confirm in modal
When I visit project "Shop" activity page
Then I should see deleted milestone activity
@javascript
Scenario: I delete new milestone
Given I click link to remove milestone
Given I click button to remove milestone
And I confirm in modal
And I should see no milestones
@javascript
......
......@@ -3,7 +3,6 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include SharedMarkdown
include CapybaraHelpers
step 'I should see milestone "v2.2"' do
milestone = @project.milestones.find_by(title: "v2.2")
......@@ -65,8 +64,12 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
expect(page).to have_selector('#tab-issues li.issuable-row', count: 4)
end
step 'I click link to remove milestone' do
confirm_modal_if_present { click_link 'Delete' }
step 'I click button to remove milestone' do
click_button 'Delete'
end
step 'I confirm in modal' do
click_button 'Delete milestone'
end
step 'I should see no milestones' do
......
module CapybaraHelpers
def confirm_modal_if_present
if Capybara.current_driver == Capybara.javascript_driver
accept_confirm { yield }
return
end
yield
end
end
......@@ -115,6 +115,7 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::Applications
mount ::API::AwardEmoji
mount ::API::Boards
mount ::API::Branches
......
module API
# External applications API
class Applications < Grape::API
before { authenticated_as_admin! }
resource :applications do
desc 'Create a new application' do
detail 'This feature was introduced in GitLab 10.5'
success Entities::ApplicationWithSecret
end
params do
requires :name, type: String, desc: 'Application name'
requires :redirect_uri, type: String, desc: 'Application redirect URI'
requires :scopes, type: String, desc: 'Application scopes'
end
post do
application = Doorkeeper::Application.new(declared_params)
if application.save
present application, with: Entities::ApplicationWithSecret
else
render_validation_error! application
end
end
end
end
end
......@@ -1440,5 +1440,15 @@ module API
pages_domain
end
end
class Application < Grape::Entity
expose :uid, as: :application_id
expose :redirect_uri, as: :callback_url
end
# Use with care, this exposes the secret
class ApplicationWithSecret < Application
expose :secret
end
end
end
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/953
#
module Gitlab
module BareRepositoryImport
class Repository
......
......@@ -54,6 +54,10 @@ module Gitlab
postgresql? && version.to_f >= 9.4
end
def self.pg_stat_wal_receiver_supported?
postgresql? && version.to_f >= 9.6
end
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
......
......@@ -34,7 +34,7 @@ module Gitlab
def raw(repository, sha)
Gitlab::GitalyClient.migrate(:git_blob_raw) do |is_enabled|
if is_enabled
Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
else
rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE)
end
......@@ -70,11 +70,19 @@ module Gitlab
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids)
blob_ids.lazy
.select { |sha| possible_lfs_blob?(repository, sha) }
.map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) }
.select(&:lfs_pointer?)
.force
return [] if blob_ids.empty?
repository.gitaly_migrate(:batch_lfs_pointers) do |is_enabled|
if is_enabled
repository.gitaly_blob_client.batch_lfs_pointers(blob_ids)
else
blob_ids.lazy
.select { |sha| possible_lfs_blob?(repository, sha) }
.map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) }
.select(&:lfs_pointer?)
.force
end
end
end
def binary?(data)
......@@ -258,7 +266,7 @@ module Gitlab
Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
@data = begin
if is_enabled
Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: id, limit: -1).data
repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
else
repository.lookup(id).content
end
......
......@@ -559,6 +559,8 @@ module Gitlab
return false if ancestor_id.nil? || descendant_id.nil?
merge_base_commit(ancestor_id, descendant_id) == ancestor_id
rescue Rugged::OdbError
false
end
# Returns true is +from+ is direct ancestor to +to+, otherwise false
......@@ -1109,23 +1111,6 @@ module Gitlab
target_ref
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git(args, chdir: path, env: {}, nice: false, &block)
cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice
circuit_breaker.perform do
popen(cmd, chdir, env, &block)
end
end
def run_git!(args, chdir: path, env: {}, nice: false, &block)
output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
raise GitError, output unless status.zero?
output
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git_with_timeout(args, timeout, env: {})
circuit_breaker.perform do
......@@ -1262,6 +1247,24 @@ module Gitlab
success || gitlab_projects_error
end
def delete_remote_branches(remote_name, branch_names)
success = @gitlab_projects.delete_remote_branches(remote_name, branch_names)
success || gitlab_projects_error
end
def bundle_to_disk(save_path)
gitaly_migrate(:bundle_to_disk) do |is_enabled|
if is_enabled
gitaly_repository_client.create_bundle(save_path)
else
run_git!(%W(bundle create #{save_path} --all))
end
end
true
end
# rubocop:disable Metrics/ParameterLists
def multi_action(
user, branch_name:, message:, actions:,
......@@ -1313,6 +1316,10 @@ module Gitlab
@gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self)
end
def gitaly_blob_client
@gitaly_blob_client ||= Gitlab::GitalyClient::BlobService.new(self)
end
def gitaly_conflicts_client(our_commit_oid, their_commit_oid)
Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
end
......@@ -1327,6 +1334,52 @@ module Gitlab
raise CommandError.new(e)
end
def refs_contains_sha(ref_type, sha)
args = %W(#{ref_type} --contains #{sha})
names = run_git(args).first
if names.respond_to?(:split)
names = names.split("\n").map(&:strip)
names.each do |name|
name.slice! '* '
end
names
else
[]
end
end
def search_files_by_content(query, ref)
return [] if empty? || query.blank?
offset = 2
args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
run_git(args).first.scrub.split(/^--$/)
end
def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(/^\/*/, ""))
return [] if empty? || safe_query.blank?
args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query})
run_git(args).first.lines.map(&:strip)
end
def find_commits_by_message(query, ref, path, limit, offset)
gitaly_migrate(:commits_by_message) do |is_enabled|
if is_enabled
find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
else
find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
end
end
end
private
def shell_write_ref(ref_path, ref, old_ref)
......@@ -1348,6 +1401,22 @@ module Gitlab
Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}"
end
def run_git(args, chdir: path, env: {}, nice: false, &block)
cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice
circuit_breaker.perform do
popen(cmd, chdir, env, &block)
end
end
def run_git!(args, chdir: path, env: {}, nice: false, &block)
output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
raise GitError, output unless status.zero?
output
end
def fresh_worktree?(path)
File.exist?(path) && !clean_stuck_worktree(path)
end
......@@ -2102,6 +2171,26 @@ module Gitlab
def gitlab_projects_error
raise CommandError, @gitlab_projects.output
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref
args = %W(
log #{ref} --pretty=%H --skip #{offset}
--max-count #{limit} --grep=#{query} --regexp-ignore-case
)
args = args.concat(%W(-- #{path})) if path.present?
git_log_results = run_git(args).first.lines
git_log_results.map { |c| commit(c.chomp) }.compact
end
def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
gitaly_commit_client
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
end
end
end
end
module Gitlab
module Git
class WikiPage
attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical
attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical, :formatted_data
# This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab.
......@@ -21,6 +21,7 @@ module Gitlab
@raw_data = gollum_page.raw_data
@name = gollum_page.name
@historical = gollum_page.historical?
@formatted_data = gollum_page.formatted_data if gollum_page.is_a?(Gollum::Page)
@version = version
end
......
......@@ -32,6 +32,26 @@ module Gitlab
binary: Gitlab::Git::Blob.binary?(data)
)
end
def batch_lfs_pointers(blob_ids)
request = Gitaly::GetLFSPointersRequest.new(
repository: @gitaly_repo,
blob_ids: blob_ids
)
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request)
response.flat_map do |message|
message.lfs_pointers.map do |lfs_pointer|
Gitlab::Git::Blob.new(
id: lfs_pointer.oid,
size: lfs_pointer.size,
data: lfs_pointer.data,
binary: Gitlab::Git::Blob.binary?(lfs_pointer.data)
)
end
end
end
end
end
end
......@@ -38,19 +38,27 @@ module Gitlab
from_id = case from
when NilClass
EMPTY_TREE_ID
when Rugged::Commit
from.oid
else
from
if from.respond_to?(:oid)
# This is meant to match a Rugged::Commit. This should be impossible in
# the future.
from.oid
else
from
end
end
to_id = case to
when NilClass
EMPTY_TREE_ID
when Rugged::Commit
to.oid
else
to
if to.respond_to?(:oid)
# This is meant to match a Rugged::Commit. This should be impossible in
# the future.
to.oid
else
to
end
end
request_params = diff_between_commits_request_params(from_id, to_id, options)
......
......@@ -25,6 +25,11 @@ module Gitlab
def conflicts?
list_conflict_files.any?
rescue GRPC::FailedPrecondition
# The server raises this exception when it encounters ConflictSideMissing, which
# means a conflict exists but its `theirs` or `ours` data is nil due to a non-existent
# file in one of the trees.
true
end
def resolve_conflicts(target_repository, resolution, source_branch, target_branch)
......
......@@ -161,6 +161,23 @@ module Gitlab
return response.error.b, 1
end
end
def create_bundle(save_path)
request = Gitaly::CreateBundleRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(
@storage,
:repository_service,
:create_bundle,
request,
timeout: GitalyClient.default_timeout
)
File.open(save_path, 'wb') do |f|
response.each do |message|
f.write(message.data)
end
end
end
end
end
end
......@@ -57,10 +57,7 @@ module Gitlab
end
def commit_exists?(sha)
project.repository.lookup(sha)
true
rescue Rugged::Error
false
project.repository.commit(sha).present?
end
def collection_method
......
......@@ -11,10 +11,6 @@ module Gitlab
untar_with_options(archive: archive, dir: dir, options: 'zxf')
end
def git_bundle(repo_path:, bundle_path:)
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
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))
......
......@@ -21,7 +21,7 @@ module Gitlab
def bundle_to_disk
mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
@project.repository.bundle_to_disk(@full_path)
rescue => e
@shared.error(e)
false
......
......@@ -10,7 +10,7 @@ module Gitlab
def bundle_to_disk(full_path)
mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: full_path)
@wiki.repository.bundle_to_disk(full_path)
rescue => e
@shared.error(e)
false
......
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/954
#
namespace :gitlab do
namespace :cleanup do
HASHED_REPOSITORY_NAME = '@hashed'.freeze
......
......@@ -106,18 +106,20 @@ module QA
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
end
module Settings
autoload :Common, 'qa/page/project/settings/common'
autoload :Advanced, 'qa/page/project/settings/advanced'
autoload :Main, 'qa/page/project/settings/main'
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :Runners, 'qa/page/project/settings/runners'
end
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
end
end
module Profile
......
......@@ -3,10 +3,21 @@ module QA
module Dashboard
class Projects < Page::Base
view 'app/views/dashboard/projects/index.html.haml'
view 'app/views/shared/projects/_search_form.html.haml' do
element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/
end
def go_to_project(name)
filter_by_name(name)
find_link(text: name).click
end
def filter_by_name(name)
page.within('form#project-filter-form') do
fill_in :name, with: name
end
end
end
end
end
......
......@@ -4,6 +4,7 @@ module QA
class Side < Page::Base
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :settings_item
element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'"
element :top_level_items, '.sidebar-top-level-items'
......@@ -31,6 +32,12 @@ module QA
end
end
def go_to_settings
within_sidebar do
click_on 'Settings'
end
end
private
def hover_settings
......
module QA
module Page
module Project
module Settings
class Advanced < Page::Base
view 'app/views/projects/edit.html.haml' do
element :project_path_field, 'f.text_field :path'
element :project_name_field, 'f.text_field :name'
element :rename_project_button, "f.submit 'Rename project'"
end
def rename_to(path)
fill_project_name(path)
fill_project_path(path)
rename_project!
end
def fill_project_path(path)
fill_in :project_path, with: path
end
def fill_project_name(name)
fill_in :project_name, with: name
end
def rename_project!
click_on 'Rename project'
end
end
end
end
end
end
......@@ -3,20 +3,23 @@ module QA
module Project
module Settings
module Common
def expand(element_name)
page.within('#content-body') do
click_element(element_name)
yield
def self.included(base)
base.class_eval do
view 'app/views/projects/edit.html.haml' do
element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'"
end
end
end
# Click the Expand button present in the specified section
#
# @param [String] name present in the container in the DOM
def expand_section(name)
page.within('#content-body') do
page.within('section', text: name) do
click_button 'Expand'
click_button('Expand')
yield
yield if block_given?
end
end
end
......
module QA
module Page
module Project
module Settings
class Main < Page::Base
include Common
view 'app/views/projects/edit.html.haml' do
element :advanced_settings_section, 'Advanced settings'
end
def expand_advanced_settings(&block)
expand_section('Advanced settings') do
Advanced.perform(&block)
end
end
end
end
end
end
end
......@@ -6,11 +6,11 @@ module QA
include Common
view 'app/views/projects/deploy_keys/_index.html.haml' do
element :expand_deploy_keys
element :deploy_keys_section, 'Deploy Keys'
end
def expand_deploy_keys(&block)
expand(:expand_deploy_keys) do
expand_section('Deploy Keys') do
DeployKeys.perform(&block)
end
end
......
......@@ -23,11 +23,11 @@ module QA
# In case of an address that is a symbol we will try to guess address
# based on `Runtime::Scenario#something_address`.
#
def visit(address, page, &block)
def visit(address, page = nil, &block)
Browser::Session.new(address, page).perform(&block)
end
def self.visit(address, page, &block)
def self.visit(address, page = nil, &block)
new.visit(address, page, &block)
end
......
module QA
feature 'GitLab Geo project rename replication', :geo do
scenario 'user renames project' do
# create the project and push code
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
project = Factory::Resource::Project.fabricate! do |project|
project.name = 'geo-before-rename'
project.description = 'Geo project to be renamed'
end
geo_project_name = project.name
expect(project.name).to include 'geo-before-rename'
Factory::Repository::Push.fabricate! do |push|
push.project = project
push.file_name = 'README.md'
push.file_content = '# This is Geo project!'
push.commit_message = 'Add README.md'
end
# rename the project
Page::Menu::Main.act { go_to_projects }
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(geo_project_name)
end
Page::Menu::Side.act { go_to_settings }
geo_project_renamed = "geo-after-rename-#{SecureRandom.hex(8)}"
Page::Project::Settings::Main.perform do |settings|
settings.expand_advanced_settings do |page|
page.rename_to(geo_project_renamed)
end
end
sleep 2 # wait for replication
# check renamed project exist on secondary node
Runtime::Browser.visit(:geo_secondary, QA::Page::Main::Login) do
Page::Main::OAuth.act do
authorize! if needs_authorization?
end
expect(page).to have_content 'You are on a secondary (read-only) Geo node'
Page::Menu::Main.perform do |menu|
menu.go_to_projects
expect(page).to have_content(geo_project_renamed)
end
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(geo_project_renamed)
end
Page::Project::Show.perform do
expect(page).to have_content 'README.md'
expect(page).to have_content 'This is Geo project!'
end
end
end
end
end
end
......@@ -4,7 +4,7 @@ module QA
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Project.fabricate! do |project|
project = Factory::Resource::Project.fabricate! do |project|
project.name = 'geo-project'
project.description = 'Geo test project'
end
......@@ -12,21 +12,11 @@ module QA
geo_project_name = Page::Project::Show.act { project_name }
expect(geo_project_name).to include 'geo-project'
Git::Repository.perform do |repository|
repository.location = Page::Project::Show.act do
choose_repository_clone_http
repository_location
end
repository.use_default_credentials
repository.act do
clone
configure_identity('GitLab QA', 'root@gitlab.com')
add_file('README.md', '# This is Geo project!')
commit('Add README.md')
push_changes
end
Factory::Repository::Push.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is Geo project!'
push.commit_message = 'Add README.md'
push.project = project
end
Runtime::Browser.visit(:geo_secondary, QA::Page::Main::Login) do
......
......@@ -9,11 +9,21 @@ require 'fileutils'
# called 'bundle install' using a different Gemfile, as happens with
# gitlab-ce and gitaly.
dir = 'tmp/tests/gitaly'
tmp_tests_gitaly_dir = File.expand_path('../tmp/tests/gitaly', __dir__)
abort 'gitaly build failed' unless system('make', chdir: dir)
# Use the top-level bundle vendor folder so that we don't reinstall gems twice
bundle_vendor_path = File.expand_path('../vendor', __dir__)
env = {
# This ensure the `clean` config set in `scripts/prepare_build.sh` isn't taken into account
'BUNDLE_IGNORE_CONFIG' => 'true',
'BUNDLE_GEMFILE' => File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile'),
'BUNDLE_FLAGS' => "--jobs=4 --path=#{bundle_vendor_path} --retry=3"
}
abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
# Without this a gitaly executable created in the setup-test-env job
# will look stale compared to GITALY_SERVER_VERSION.
FileUtils.touch(File.join(dir, 'gitaly'), mtime: Time.now + (1 << 24))
FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24))
#!/usr/bin/env ruby
ALLOWED = [
# https://gitlab.com/gitlab-org/gitaly/issues/760
'lib/elasticsearch/git/repository.rb',
# Can be deleted (?) once rugged is no longer used in production. Doesn't make Rugged calls.
'config/initializers/8_metrics.rb',
# Can be deleted once wiki's are fully (mandatory) migrated
'config/initializers/gollum.rb',
# Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/953
'lib/gitlab/bare_repository_import/repository.rb',
# Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/954
'lib/tasks/gitlab/cleanup.rake',
# https://gitlab.com/gitlab-org/gitaly/issues/961
'app/models/repository.rb',
# The only place where Rugged code is still allowed in production
'lib/gitlab/git/'
].freeze
rugged_lines = IO.popen(%w[git grep -i -n rugged -- app config lib], &:read).lines
rugged_lines = rugged_lines.reject { |l| l.start_with?(*ALLOWED) }
rugged_lines = rugged_lines.reject do |line|
code, _comment = line.split('# ', 2)
code !~ /rugged/i
end
exit if rugged_lines.empty?
puts "Using Rugged is only allowed in test and #{ALLOWED}\n\n"
puts rugged_lines
exit(false)
......@@ -3,7 +3,7 @@
export SETUP_DB=${SETUP_DB:-true}
export CREATE_DB_USER=${CREATE_DB_USER:-$SETUP_DB}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
export BUNDLE_INSTALL_FLAGS="--without=production --jobs=$(nproc) --path=vendor --retry=3 --quiet"
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
......
......@@ -13,7 +13,8 @@ tasks = [
%w[bundle exec rake gettext:lint],
%w[bundle exec rake lint:static_verification],
%w[scripts/lint-changelog-yaml],
%w[scripts/lint-conflicts.sh]
%w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged]
]
failed_tasks = tasks.reduce({}) do |failures, task|
......
......@@ -5,7 +5,8 @@ describe 'Issue Boards', :js do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let!(:milestone) { create(:milestone, project: project) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
......@@ -141,6 +142,36 @@ describe 'Issue Boards', :js do
end
end
context 'epic' do
before do
stub_licensed_features(epics: true)
visit project_board_path(project, board)
wait_for_requests
end
context 'when the issue is not associated with an epic' do
it 'displays `None` for value of epic' do
click_card(card1)
wait_for_requests
expect(find('.js-epic-label').text).to have_content('None')
end
end
context 'when the issue is associated with an epic' do
let(:epic) { create(:epic, group: group) }
let!(:epic_issue) { create(:epic_issue, issue: issue1, epic: epic) }
it 'displays name of epic and links to it' do
click_card(card1)
wait_for_requests
expect(find('.js-epic-label')).to have_link(epic.title, href: epic_path(epic))
end
end
end
context 'weight' do
it 'displays weight async' do
click_card(card1)
......
......@@ -15,7 +15,7 @@ describe Gitlab::Geo::HealthCheck, :postgresql do
allow(described_class).to receive(:database_secondary?).and_return(true)
allow(described_class).to receive(:get_database_version).and_return('20170101')
allow(described_class).to receive(:get_migration_version).and_return('20170201')
allow(described_class).to receive(:db_replication_lag_seconds).and_return(0)
allow(described_class).to receive(:streaming_active?).and_return(true)
message = subject.perform_checks
......@@ -54,7 +54,7 @@ describe Gitlab::Geo::HealthCheck, :postgresql do
it 'returns an error when Geo database version does not match the latest migration version' do
allow(described_class).to receive(:database_secondary?).and_return(true)
allow(subject).to receive(:get_database_version) { 1 }
allow(described_class).to receive(:db_replication_lag_seconds).and_return(0)
allow(described_class).to receive(:streaming_active?).and_return(true)
expect(subject.perform_checks).to match(/Current Geo database version \([0-9]+\) does not match latest migration \([0-9]+\)/)
end
......@@ -62,17 +62,26 @@ describe Gitlab::Geo::HealthCheck, :postgresql do
it 'returns an error when latest migration version does not match the Geo database version' do
allow(described_class).to receive(:database_secondary?).and_return(true)
allow(subject).to receive(:get_migration_version) { 1 }
allow(described_class).to receive(:db_replication_lag_seconds).and_return(0)
allow(described_class).to receive(:streaming_active?).and_return(true)
expect(subject.perform_checks).to match(/Current Geo database version \([0-9]+\) does not match latest migration \([0-9]+\)/)
end
it 'returns an error when replication lag is not present' do
it 'returns an error when streaming is not active and Postgresql supports pg_stat_wal_receiver' do
allow(Gitlab::Database).to receive(:pg_stat_wal_receiver_supported?).and_return(true)
allow(described_class).to receive(:database_secondary?).and_return(true)
allow(described_class).to receive(:db_replication_lag_seconds).and_return(nil)
allow(described_class).to receive(:streaming_active?).and_return(false)
expect(subject.perform_checks).to match(/The Geo node does not appear to be replicating data from the primary node/)
end
it 'returns an error when streaming is not active and Postgresql does not support pg_stat_wal_receiver' do
allow(Gitlab::Database).to receive(:pg_stat_wal_receiver_supported?).and_return(false)
allow(described_class).to receive(:database_secondary?).and_return(true)
allow(described_class).to receive(:streaming_active?).and_return(false)
expect(subject.perform_checks).to be_empty
end
end
describe 'MySQL checks' do
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171103152048_geo_drain_redis_queues')
require Rails.root.join('ee', 'db', 'post_migrate', '20171103152048_geo_drain_redis_queues')
describe GeoDrainRedisQueues, :clean_gitlab_redis_shared_state do
subject(:migration) { described_class.new }
......
require "spec_helper"
require Rails.root.join("db", "post_migrate", "20170811082658_remove_system_hook_from_geo_nodes.rb")
require 'spec_helper'
require Rails.root.join('ee', 'db', 'post_migrate', '20170811082658_remove_system_hook_from_geo_nodes.rb')
describe RemoveSystemHookFromGeoNodes, :migration do
let(:geo_nodes) { table(:geo_nodes) }
......
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20170626202753_update_authorized_keys_file.rb')
require Rails.root.join('ee', 'db', 'migrate', '20170626202753_update_authorized_keys_file.rb')
describe UpdateAuthorizedKeysFile, :migration do
let(:migration) { described_class.new }
......
......@@ -474,7 +474,9 @@ describe GeoNodeStatus, :geo do
end
describe '#storage_shards_match?' do
before { stub_primary_node }
before do
stub_primary_node
end
set(:status) { create(:geo_node_status) }
let(:data) { GeoNodeStatusSerializer.new.represent(status).as_json }
......
......@@ -32,7 +32,9 @@ describe Geo::RepositorySyncService do
it 'fetches project repository with JWT credentials' do
expect(repository).to receive(:with_config).with("http.#{url_to_repo}.extraHeader" => anything).and_call_original
expect(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true).once
expect(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.once
subject.execute
end
......@@ -59,7 +61,9 @@ describe Geo::RepositorySyncService do
end
it 'returns the lease when sync fail' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Shell::Error }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Shell::Error)
expect(Gitlab::ExclusiveLease).to receive(:cancel).once.with(
subject.__send__(:lease_key), anything).and_call_original
......@@ -76,20 +80,25 @@ describe Geo::RepositorySyncService do
end
it 'rescues when Gitlab::Shell::Error is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Shell::Error }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Shell::Error)
expect { subject.execute }.not_to raise_error
end
it 'rescues when Gitlab::Git::RepositoryMirroring::RemoteError is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true)
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Git::RepositoryMirroring::RemoteError)
expect { subject.execute }.not_to raise_error
end
it 'rescues exception and fires after_create hook when Gitlab::Git::Repository::NoRepository is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Git::Repository::NoRepository }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Git::Repository::NoRepository)
expect(repository).to receive(:after_create)
......@@ -97,7 +106,9 @@ describe Geo::RepositorySyncService do
end
it 'increases retry count when Gitlab::Git::Repository::NoRepository is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Git::Repository::NoRepository }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Git::Repository::NoRepository)
subject.execute
......@@ -161,7 +172,9 @@ describe Geo::RepositorySyncService do
let(:registry) { Geo::ProjectRegistry.find_by(project_id: project.id) }
before do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Shell::Error, 'shell error' }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Shell::Error.new('shell error'))
subject.execute
end
......
......@@ -32,7 +32,9 @@ RSpec.describe Geo::WikiSyncService do
it 'fetches wiki repository with JWT credentials' do
expect(repository).to receive(:with_config).with("http.#{url_to_repo}.extraHeader" => anything).and_call_original
expect(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true).once
expect(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.once
subject.execute
end
......@@ -59,26 +61,33 @@ RSpec.describe Geo::WikiSyncService do
end
it 'rescues exception when Gitlab::Shell::Error is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Shell::Error }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Shell::Error)
expect { subject.execute }.not_to raise_error
end
it 'rescues exception when Gitlab::Git::RepositoryMirroring::RemoteError is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true)
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Git::RepositoryMirroring::RemoteError)
expect { subject.execute }.not_to raise_error
end
it 'rescues exception when Gitlab::Git::Repository::NoRepository is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Git::Repository::NoRepository }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Git::Repository::NoRepository)
expect { subject.execute }.not_to raise_error
end
it 'increases retry count when Gitlab::Git::Repository::NoRepository is raised' do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Git::Repository::NoRepository }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Git::Repository::NoRepository)
subject.execute
......@@ -123,7 +132,9 @@ RSpec.describe Geo::WikiSyncService do
let(:registry) { Geo::ProjectRegistry.find_by(project_id: project.id) }
before do
allow(repository).to receive(:fetch_as_mirror).with(url_to_repo, forced: true) { raise Gitlab::Shell::Error, 'shell error' }
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Shell::Error.new('shell error'))
subject.execute
end
......
import SecretValues from '~/behaviors/secret_values';
function generateFixtureMarkup(secrets, isRevealed) {
function generateValueMarkup(
secret,
valueClass = 'js-secret-value',
placeholderClass = 'js-secret-value-placeholder',
) {
return `
<div class="${placeholderClass}">
***
</div>
<div class="hide ${valueClass}">
${secret}
</div>
`;
}
function generateFixtureMarkup(secrets, isRevealed, valueClass, placeholderClass) {
return `
<div class="js-secret-container">
${secrets.map(secret => `
<div class="js-secret-value-placeholder">
***
</div>
<div class="hide js-secret-value">
${secret}
</div>
`).join('')}
${secrets.map(secret => generateValueMarkup(secret, valueClass, placeholderClass)).join('')}
<button
class="js-secret-value-reveal-button"
data-secret-reveal-status="${isRevealed}"
......@@ -21,11 +29,25 @@ function generateFixtureMarkup(secrets, isRevealed) {
`;
}
function setupSecretFixture(secrets, isRevealed) {
function setupSecretFixture(
secrets,
isRevealed,
valueClass = 'js-secret-value',
placeholderClass = 'js-secret-value-placeholder',
) {
const wrapper = document.createElement('div');
wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed);
const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container'));
wrapper.innerHTML = generateFixtureMarkup(
secrets,
isRevealed,
valueClass,
placeholderClass,
);
const secretValues = new SecretValues({
container: wrapper.querySelector('.js-secret-container'),
valueSelector: `.${valueClass}`,
placeholderSelector: `.${placeholderClass}`,
});
secretValues.init();
return wrapper;
......@@ -49,7 +71,7 @@ describe('setupSecretValues', () => {
expect(revealButton.textContent).toEqual('Hide value');
});
it('should value hidden initially', () => {
it('should have value hidden initially', () => {
const wrapper = setupSecretFixture(secrets, false);
const values = wrapper.querySelectorAll('.js-secret-value');
const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
......@@ -143,4 +165,64 @@ describe('setupSecretValues', () => {
});
});
});
describe('with dynamic secrets', () => {
const secrets = ['mysecret123', 'happygoat456', 'tanuki789'];
it('should toggle values and placeholders', () => {
const wrapper = setupSecretFixture(secrets, false);
// Insert the new dynamic row
wrapper.querySelector('.js-secret-container').insertAdjacentHTML('afterbegin', generateValueMarkup('foobarbazdynamic'));
const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
const values = wrapper.querySelectorAll('.js-secret-value');
const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
revealButton.click();
expect(values.length).toEqual(4);
values.forEach((value) => {
expect(value.classList.contains('hide')).toEqual(false);
});
expect(placeholders.length).toEqual(4);
placeholders.forEach((placeholder) => {
expect(placeholder.classList.contains('hide')).toEqual(true);
});
revealButton.click();
expect(values.length).toEqual(4);
values.forEach((value) => {
expect(value.classList.contains('hide')).toEqual(true);
});
expect(placeholders.length).toEqual(4);
placeholders.forEach((placeholder) => {
expect(placeholder.classList.contains('hide')).toEqual(false);
});
});
});
describe('selector options', () => {
const secrets = ['mysecret123'];
it('should respect `valueSelector` and `placeholderSelector` options', () => {
const valueClass = 'js-some-custom-placeholder-selector';
const placeholderClass = 'js-some-custom-value-selector';
const wrapper = setupSecretFixture(secrets, false, valueClass, placeholderClass);
const values = wrapper.querySelectorAll(`.${valueClass}`);
const placeholders = wrapper.querySelectorAll(`.${placeholderClass}`);
const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
expect(values.length).toEqual(1);
expect(placeholders.length).toEqual(1);
revealButton.click();
expect(values.length).toEqual(1);
expect(values[0].classList.contains('hide')).toEqual(false);
expect(placeholders.length).toEqual(1);
expect(placeholders[0].classList.contains('hide')).toEqual(true);
});
});
});
......@@ -12,6 +12,10 @@ describe('JobMediator', () => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('should set defaults', () => {
expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined();
......@@ -24,10 +28,6 @@ describe('JobMediator', () => {
mock.onGet().reply(200, job, {});
});
afterEach(() => {
mock.restore();
});
it('should store received data', (done) => {
mediator.fetchJob();
setTimeout(() => {
......
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from '../../../../helpers/vue_mount_component_helper';
describe('delete_milestone_modal.vue', () => {
const Component = Vue.extend(deleteMilestoneModal);
const props = {
issueCount: 1,
mergeRequestCount: 2,
milestoneId: 3,
milestoneTitle: 'my milestone title',
milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`,
};
let vm;
afterEach(() => {
vm.$destroy();
});
describe('onSubmit', () => {
beforeEach(() => {
vm = mountComponent(Component, props);
spyOn(eventHub, '$emit');
});
it('deletes milestone and redirects to overview page', (done) => {
const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
spyOn(axios, 'delete').and.callFake((url) => {
expect(url).toBe(props.milestoneUrl);
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl);
eventHub.$emit.calls.reset();
return Promise.resolve({
request: {
responseURL,
},
});
});
const redirectSpy = spyOn(urlUtility, 'redirectTo');
vm.onSubmit()
.then(() => {
expect(redirectSpy).toHaveBeenCalledWith(responseURL);
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: true });
})
.then(done)
.catch(done.fail);
});
it('displays error if deleting milestone failed', (done) => {
const dummyError = new Error('deleting milestone failed');
dummyError.response = { status: 418 };
spyOn(axios, 'delete').and.callFake((url) => {
expect(url).toBe(props.milestoneUrl);
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl);
eventHub.$emit.calls.reset();
return Promise.reject(dummyError);
});
const redirectSpy = spyOn(urlUtility, 'redirectTo');
vm.onSubmit()
.catch((error) => {
expect(error).toBe(dummyError);
expect(redirectSpy).not.toHaveBeenCalled();
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: false });
})
.then(done)
.catch(done.fail);
});
});
describe('text', () => {
it('contains the issue and milestone count', () => {
vm = mountComponent(Component, props);
const value = vm.text;
expect(value).toContain('remove it from 1 issue and 2 merge requests');
});
it('contains neither issue nor milestone count', () => {
vm = mountComponent(Component, { ...props,
issueCount: 0,
mergeRequestCount: 0,
});
const value = vm.text;
expect(value).toContain('is not currently used');
});
});
});
......@@ -9,7 +9,9 @@ describe API::Entities::GeoNodeStatus, :postgresql do
subject { entity.as_json }
before { stub_primary_node }
before do
stub_primary_node
end
describe '#healthy' do
context 'when node is healthy' do
......@@ -127,7 +129,9 @@ describe API::Entities::GeoNodeStatus, :postgresql do
end
context 'secondary Geo node' do
before { stub_secondary_node }
before do
stub_secondary_node
end
it { is_expected.to have_key(:storage_shards) }
it { is_expected.not_to have_key(:storage_shards_match) }
......
......@@ -260,29 +260,42 @@ describe Gitlab::Git::Blob, seed_helper: true do
)
end
it 'returns a list of Gitlab::Git::Blob' do
blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
shared_examples 'fetching batch of LFS pointers' do
it 'returns a list of Gitlab::Git::Blob' do
blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
expect(blobs.count).to eq(1)
expect(blobs).to all( be_a(Gitlab::Git::Blob) )
end
expect(blobs.count).to eq(1)
expect(blobs).to all( be_a(Gitlab::Git::Blob) )
end
it 'silently ignores tree objects' do
blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
it 'silently ignores tree objects' do
blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
expect(blobs).to eq([])
end
expect(blobs).to eq([])
end
it 'silently ignores non lfs objects' do
blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
it 'silently ignores non lfs objects' do
blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
expect(blobs).to eq([])
end
it 'avoids loading large blobs into memory' do
# This line could call `lookup` on `repository`, so do here before mocking.
non_lfs_blob_id = non_lfs_blob.id
expect(repository).not_to receive(:lookup)
expect(blobs).to eq([])
described_class.batch_lfs_pointers(repository, [non_lfs_blob_id])
end
end
it 'avoids loading large blobs into memory' do
expect(repository).not_to receive(:lookup)
context 'when Gitaly batch_lfs_pointers is enabled' do
it_behaves_like 'fetching batch of LFS pointers'
end
described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
context 'when Gitaly batch_lfs_pointers is disabled', :disable_gitaly do
it_behaves_like 'fetching batch of LFS pointers'
end
end
......
......@@ -1926,6 +1926,34 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { expect(subject.repository_relative_path).to eq(repository.relative_path) }
end
describe '#bundle_to_disk' do
shared_examples 'bundling to disk' do
let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
after do
FileUtils.rm_rf(save_path)
end
it 'saves a bundle to disk' do
repository.bundle_to_disk(save_path)
success = system(
*%W(#{Gitlab.config.git.bin_path} -C #{repository.path} bundle verify #{save_path}),
[:out, :err] => '/dev/null'
)
expect(success).to be true
end
end
context 'when Gitaly bundle_to_disk feature is enabled' do
it_behaves_like 'bundling to disk'
end
context 'when Gitaly bundle_to_disk feature is disabled', :disable_gitaly do
it_behaves_like 'bundling to disk'
end
end
context 'gitlab_projects commands' do
let(:gitlab_projects) { repository.gitlab_projects }
let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
......
......@@ -244,7 +244,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
it 'returns true when a commit exists' do
expect(project.repository)
.to receive(:lookup)
.to receive(:commit)
.with('123')
.and_return(double(:commit))
......@@ -253,9 +253,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
it 'returns false when a commit does not exist' do
expect(project.repository)
.to receive(:lookup)
.to receive(:commit)
.with('123')
.and_raise(Rugged::OdbError)
.and_return(nil)
expect(importer.commit_exists?('123')).to eq(false)
end
......
......@@ -5,7 +5,11 @@ require 'spec_helper'
describe ActiveRecord::Schema do
let(:latest_migration_timestamp) do
migrations = Dir[Rails.root.join('db', 'migrate', '*'), Rails.root.join('db', 'post_migrate', '*')]
migrations_paths =
%w[db ee/db].product(%w[migrate post_migrate]).each_with_object([]) do |migration_dir, memo|
memo << Rails.root.join(*migration_dir, '*')
end
migrations = Dir[*migrations_paths]
migrations.map { |migration| File.basename(migration).split('_').first.to_i }.max
end
......
......@@ -3661,5 +3661,22 @@ describe Project do
project = build(:project)
project.execute_hooks({ data: 'data' }, :merge_request_hooks)
end
it 'executes the system hooks when inside a transaction' do
allow_any_instance_of(WebHookService).to receive(:execute)
create(:system_hook, merge_requests_events: true)
project = build(:project)
# Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1,
# but since the entire spec run takes place in a transaction, we never
# actually get to the `after_commit` hook that queues these jobs.
expect do
project.transaction do
project.execute_hooks({ data: 'data' }, :merge_request_hooks)
end
end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
end
end
end
......@@ -365,12 +365,18 @@ describe Repository do
it { is_expected.to be_truthy }
end
context 'non-mergeable branches' do
context 'non-mergeable branches without conflict sides missing' do
subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') }
it { is_expected.to be_falsey }
end
context 'non-mergeable branches with conflict sides missing' do
subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') }
it { is_expected.to be_falsey }
end
context 'non merged branch' do
subject { repository.merged_to_root_ref?('fix') }
......@@ -2472,7 +2478,7 @@ describe Repository do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
context 'with Gitaly enabled' do
shared_examples '#ancestor?' do
it 'it is an ancestor' do
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
......@@ -2486,27 +2492,19 @@ describe Repository do
expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
expect(repository.ancestor?(nil, nil)).to eq(false)
end
end
context 'with Gitaly disabled' do
before do
allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false)
allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false)
end
it 'it is an ancestor' do
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
it 'returns false for invalid commit IDs' do
expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
end
end
it 'it is not an ancestor' do
expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
end
context 'with Gitaly enabled' do
it_behaves_like('#ancestor?')
end
it 'returns false on nil-values' do
expect(repository.ancestor?(nil, commit.id)).to eq(false)
expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
expect(repository.ancestor?(nil, nil)).to eq(false)
end
context 'with Gitaly disabled', :skip_gitaly_mock do
it_behaves_like('#ancestor?')
end
end
......
......@@ -386,6 +386,17 @@ describe WikiPage do
end
end
describe '#formatted_content' do
it 'returns processed content of the page', :disable_gitaly do
subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" })
page = wiki.find_page('RDoc')
expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n")
destroy_page('RDoc')
end
end
private
def remove_temp_repo(path)
......
require 'spec_helper'
describe API::Applications, :api do
include ApiHelpers
let(:admin_user) { create(:user, admin: true) }
let(:user) { create(:user, admin: false) }
describe 'POST /applications' do
context 'authenticated and authorized user' do
it 'creates and returns an OAuth application' do
expect do
post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url', scopes: ''
end.to change { Doorkeeper::Application.count }.by 1
application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url')
expect(response).to have_http_status 201
expect(json_response).to be_a Hash
expect(json_response['application_id']).to eq application.uid
expect(json_response['secret']).to eq application.secret
expect(json_response['callback_url']).to eq application.redirect_uri
end
it 'does not allow creating an application with the wrong redirect_uri format' do
expect do
post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: ''
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_http_status 400
expect(json_response).to be_a Hash
expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.')
end
it 'does not allow creating an application without a name' do
expect do
post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: ''
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_http_status 400
expect(json_response).to be_a Hash
expect(json_response['error']).to eq('name is missing')
end
it 'does not allow creating an application without a redirect_uri' do
expect do
post api('/applications', admin_user), name: 'application_name', scopes: ''
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_http_status 400
expect(json_response).to be_a Hash
expect(json_response['error']).to eq('redirect_uri is missing')
end
it 'does not allow creating an application without scopes' do
expect do
post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url'
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_http_status 400
expect(json_response).to be_a Hash
expect(json_response['error']).to eq('scopes is missing')
end
end
context 'authorized user without authorization' do
it 'does not create application' do
expect do
post api('/applications', user), name: 'application_name', redirect_uri: 'http://application.url', scopes: ''
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_http_status 403
end
end
context 'non-authenticated user' do
it 'does not create application' do
expect do
post api('/applications'), name: 'application_name', redirect_uri: 'http://application.url'
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_http_status 401
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