Commit 581c10e3 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 68d3f33d
...@@ -116,16 +116,20 @@ export default { ...@@ -116,16 +116,20 @@ export default {
// We have 10+ awarded user, join them with comma and add `and x more`. // We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) { if (remainingAwardList.length) {
title = sprintf(__(`%{listToShow}, and %{awardsListLength} more.`), { title = sprintf(
listToShow: namesToShow.join(', '), __(`%{listToShow}, and %{awardsListLength} more.`),
awardsListLength: remainingAwardList.length, {
}); listToShow: namesToShow.join(', '),
awardsListLength: remainingAwardList.length,
},
false,
);
} else if (namesToShow.length > 1) { } else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text. // Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', '); title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text. // If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : ''; title += namesToShow.length > 2 ? ',' : '';
title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }); // Append and text title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }, false); // Append and text
} else { } else {
// We have only 2 users so join them with and. // We have only 2 users so join them with and.
title = namesToShow.join(__(' and ')); title = namesToShow.join(__(' and '));
......
...@@ -1052,18 +1052,19 @@ class Repository ...@@ -1052,18 +1052,19 @@ class Repository
return rebase_deprecated(user, merge_request) return rebase_deprecated(user, merge_request)
end end
MergeRequest.transaction do raw.rebase(
raw.rebase( user,
user, merge_request.id,
merge_request.id, branch: merge_request.source_branch,
branch: merge_request.source_branch, branch_sha: merge_request.source_branch_sha,
branch_sha: merge_request.source_branch_sha, remote_repository: merge_request.target_project.repository.raw,
remote_repository: merge_request.target_project.repository.raw, remote_branch: merge_request.target_branch
remote_branch: merge_request.target_branch ) do |commit_id|
) do |commit_id| merge_request.update!(rebase_commit_sha: commit_id, merge_error: nil)
merge_request.update!(rebase_commit_sha: commit_id, merge_error: nil)
end
end end
rescue StandardError => error
merge_request.update!(rebase_commit_sha: nil)
raise error
end end
def squash(user, merge_request, message) def squash(user, merge_request, message)
......
...@@ -13,7 +13,6 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -13,7 +13,6 @@ class PersonalSnippetPolicy < BasePolicy
rule { is_author | admin }.policy do rule { is_author | admin }.policy do
enable :read_personal_snippet enable :read_personal_snippet
enable :update_personal_snippet enable :update_personal_snippet
enable :destroy_personal_snippet
enable :admin_personal_snippet enable :admin_personal_snippet
enable :create_note enable :create_note
end end
......
---
title: Fix award emoji tooltip being escaped twice if multiple people voted
merge_request: 19273
author: Brian T
type: fixed
---
title: Remove destroy_personal_snippet ability
merge_request: 20717
author:
type: fixed
---
title: Remove DB transaction from Rebase operation
merge_request: 20739
author:
type: fixed
...@@ -31,7 +31,7 @@ enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' ...@@ -31,7 +31,7 @@ enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
enable_sidekiq_memory_killer = ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'].to_i.nonzero? enable_sidekiq_memory_killer = ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'].to_i.nonzero?
use_sidekiq_daemon_memory_killer = ENV["SIDEKIQ_DAEMON_MEMORY_KILLER"].to_i.nonzero? use_sidekiq_daemon_memory_killer = ENV["SIDEKIQ_DAEMON_MEMORY_KILLER"].to_i.nonzero?
use_sidekiq_legacy_memory_killer = !use_sidekiq_daemon_memory_killer use_sidekiq_legacy_memory_killer = !use_sidekiq_daemon_memory_killer
use_request_store = ENV['SIDEKIQ_REQUEST_STORE'].to_i.nonzero? use_request_store = ENV.fetch('SIDEKIQ_REQUEST_STORE', 1).to_i.nonzero?
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = queues_config_hash config.redis = queues_config_hash
......
...@@ -51,14 +51,16 @@ def presented_no_changelog_labels ...@@ -51,14 +51,16 @@ def presented_no_changelog_labels
NO_CHANGELOG_LABELS.map { |label| "~#{label}" }.join(', ') NO_CHANGELOG_LABELS.map { |label| "~#{label}" }.join(', ')
end end
def sanitized_mr_title
gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`')
end
changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
mr_title = gitlab.mr_json["title"].gsub(/^WIP: */, '')
if git.modified_files.include?("CHANGELOG.md") if git.modified_files.include?("CHANGELOG.md")
fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" + fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" +
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: mr_title, labels: presented_no_changelog_labels) format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: presented_no_changelog_labels)
end end
if changelog_needed if changelog_needed
...@@ -66,6 +68,6 @@ if changelog_needed ...@@ -66,6 +68,6 @@ if changelog_needed
check_changelog(changelog_found) check_changelog(changelog_found)
else else
message "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html)**: If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.\n\n" + message "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html)**: If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.\n\n" +
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: mr_title, labels: presented_no_changelog_labels) format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: presented_no_changelog_labels)
end end
end end
...@@ -49,6 +49,13 @@ branch names globally in Push Rules, you can now sleep without the anxiety ...@@ -49,6 +49,13 @@ branch names globally in Push Rules, you can now sleep without the anxiety
of your developers' mistakes. Every branch that doesn't match your push rule of your developers' mistakes. Every branch that doesn't match your push rule
will get rejected. will get rejected.
### Custom Push Rules **(CORE ONLY)**
It's possible to create custom push rules rather than the push rules available in
**Admin area > Push Rules** by using more advanced server-side Git hooks.
See [custom server-side Git hooks](../administration/custom_hooks.md) for more information.
## Enabling push rules ## Enabling push rules
NOTE: **Note:** NOTE: **Note:**
......
...@@ -75,7 +75,7 @@ To define specs for each environment: ...@@ -75,7 +75,7 @@ To define specs for each environment:
1. Set the status and rollout strategy of the additional spec. This status and rollout strategy combination takes precedence over the default spec since we always use the most specific match available. 1. Set the status and rollout strategy of the additional spec. This status and rollout strategy combination takes precedence over the default spec since we always use the most specific match available.
1. Click **Create feature flag** or **Update feature flag**. 1. Click **Create feature flag** or **Update feature flag**.
![Feature flag specs list](img/specs_list.png) ![Feature flag specs list](img/specs_list_v12_6.png)
NOTE: **NOTE** NOTE: **NOTE**
We'd highly recommend you to use the [Environment](../../../ci/environments.md) We'd highly recommend you to use the [Environment](../../../ci/environments.md)
...@@ -119,17 +119,15 @@ CAUTION: **Caution:** ...@@ -119,17 +119,15 @@ CAUTION: **Caution:**
If this strategy is selected, then the Unleash client **must** be given a user If this strategy is selected, then the Unleash client **must** be given a user
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below. ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
### Target users strategy #### User IDs
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8240) in GitLab 12.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8240) in GitLab 12.2. [Updated](https://gitlab.com/gitlab-org/gitlab/issues/34363) to be defined per environment in GitLab 12.6.
A feature flag may be enabled for a list of target users. It is implemented A feature flag may be enabled for a list of target users. It is implemented
using the Unleash [`userWithId`](https://unleash.github.io/docs/activation_strategy#userwithid) using the Unleash [`userWithId`](https://unleash.github.io/docs/activation_strategy#userwithid)
activation strategy. activation strategy.
The feature will always be enabled for all users in the list across all environments even if the matching environment spec **Status** is disabled. User IDs should be a comma separated list of values. For example, `user@example.com, user2@example.com`, or `username1,username2,username3`, etc.
![Feature flag target users](img/target_users_v12_2.png)
CAUTION: **Caution:** CAUTION: **Caution:**
The Unleash client **must** be given a user ID for the feature to be enabled for The Unleash client **must** be given a user ID for the feature to be enabled for
......
...@@ -131,7 +131,7 @@ module API ...@@ -131,7 +131,7 @@ module API
snippet = snippets_for_current_user.find_by_id(params.delete(:id)) snippet = snippets_for_current_user.find_by_id(params.delete(:id))
break not_found!('Snippet') unless snippet break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet authorize! :admin_personal_snippet, snippet
destroy_conditionally!(snippet) destroy_conditionally!(snippet)
end end
......
import { shallowMount } from '@vue/test-utils';
import MonitoringComponent from '~/environments/components/environment_monitoring.vue';
import Icon from '~/vue_shared/components/icon.vue';
describe('Monitoring Component', () => {
let wrapper;
const monitoringUrl = 'https://gitlab.com';
const createWrapper = () => {
wrapper = shallowMount(MonitoringComponent, {
sync: false,
attachToDocument: true,
propsData: {
monitoringUrl,
},
});
};
const findIcons = () => wrapper.findAll(Icon);
const findIconsByName = name => findIcons().filter(icon => icon.props('name') === name);
beforeEach(() => {
createWrapper();
});
describe('computed', () => {
it('title', () => {
expect(wrapper.vm.title).toBe('Monitoring');
});
});
it('should render a link to environment monitoring page', () => {
expect(wrapper.attributes('href')).toEqual(monitoringUrl);
expect(findIconsByName('chart').length).toBe(1);
expect(wrapper.attributes('data-original-title')).toBe('Monitoring');
expect(wrapper.attributes('aria-label')).toBe('Monitoring');
});
});
import $ from 'jquery';
import { shallowMount } from '@vue/test-utils';
import StopComponent from '~/environments/components/environment_stop.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/environments/event_hub';
$.fn.tooltip = () => {};
describe('Stop Component', () => {
let wrapper;
const createWrapper = () => {
wrapper = shallowMount(StopComponent, {
sync: false,
attachToDocument: true,
propsData: {
environment: {},
},
});
};
const findButton = () => wrapper.find(LoadingButton);
beforeEach(() => {
jest.spyOn(window, 'confirm');
createWrapper();
});
it('should render a button to stop the environment', () => {
expect(findButton().exists()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual('Stop environment');
});
it('emits requestStopEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment);
});
});
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import terminalComp from '~/environments/components/environment_terminal_button.vue'; import TerminalComponent from '~/environments/components/environment_terminal_button.vue';
describe('Stop Component', () => { describe('Stop Component', () => {
let component; let wrapper;
const terminalPath = '/path'; const terminalPath = '/path';
const mountWithProps = props => { const mountWithProps = props => {
const TerminalComponent = Vue.extend(terminalComp); wrapper = shallowMount(TerminalComponent, {
component = new TerminalComponent({ sync: false,
attachToDocument: true,
propsData: props, propsData: props,
}).$mount(); });
}; };
beforeEach(() => { beforeEach(() => {
...@@ -18,18 +19,18 @@ describe('Stop Component', () => { ...@@ -18,18 +19,18 @@ describe('Stop Component', () => {
describe('computed', () => { describe('computed', () => {
it('title', () => { it('title', () => {
expect(component.title).toEqual('Terminal'); expect(wrapper.vm.title).toEqual('Terminal');
}); });
}); });
it('should render a link to open a web terminal with the provided path', () => { it('should render a link to open a web terminal with the provided path', () => {
expect(component.$el.tagName).toEqual('A'); expect(wrapper.is('a')).toBe(true);
expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal'); expect(wrapper.attributes('data-original-title')).toBe('Terminal');
expect(component.$el.getAttribute('aria-label')).toEqual('Terminal'); expect(wrapper.attributes('aria-label')).toBe('Terminal');
expect(component.$el.getAttribute('href')).toEqual(terminalPath); expect(wrapper.attributes('href')).toBe(terminalPath);
}); });
it('should render a non-disabled button', () => { it('should render a non-disabled button', () => {
expect(component.$el.classList).not.toContain('disabled'); expect(wrapper.classes()).not.toContain('disabled');
}); });
}); });
import Vue from 'vue';
import monitoringComp from '~/environments/components/environment_monitoring.vue';
describe('Monitoring Component', () => {
let MonitoringComponent;
let component;
const monitoringUrl = 'https://gitlab.com';
beforeEach(() => {
MonitoringComponent = Vue.extend(monitoringComp);
component = new MonitoringComponent({
propsData: {
monitoringUrl,
},
}).$mount();
});
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Monitoring');
});
});
it('should render a link to environment monitoring page', () => {
expect(component.$el.getAttribute('href')).toEqual(monitoringUrl);
expect(component.$el.querySelector('.fa-area-chart')).toBeDefined();
expect(component.$el.getAttribute('data-original-title')).toEqual('Monitoring');
expect(component.$el.getAttribute('aria-label')).toEqual('Monitoring');
});
});
import Vue from 'vue';
import stopComp from '~/environments/components/environment_stop.vue';
describe('Stop Component', () => {
let StopComponent;
let component;
beforeEach(() => {
StopComponent = Vue.extend(stopComp);
spyOn(window, 'confirm').and.returnValue(true);
component = new StopComponent({
propsData: {
environment: {},
},
}).$mount();
});
it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('data-original-title')).toEqual('Stop environment');
});
});
...@@ -61,6 +61,66 @@ describe('note_awards_list component', () => { ...@@ -61,6 +61,66 @@ describe('note_awards_list component', () => {
expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
}); });
describe('when the user name contains special HTML characters', () => {
const createAwardEmoji = (_, index) => ({
name: 'art',
user: { id: index, name: `&<>"\`'-${index}`, username: `user-${index}` },
});
const mountComponent = () => {
const Component = Vue.extend(awardsNote);
vm = new Component({
store,
propsData: {
awards: awardsMock,
noteAuthorId: 0,
noteId: '545',
canAwardEmoji: true,
toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
},
}).$mount();
};
const findTooltip = () =>
vm.$el.querySelector('[data-original-title]').getAttribute('data-original-title');
it('should only escape & and " characters', () => {
awardsMock = [...new Array(1)].map(createAwardEmoji);
mountComponent();
const escapedName = awardsMock[0].user.name.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
expect(vm.$el.querySelector('[data-original-title]').outerHTML).toContain(escapedName);
});
it('should not escape special HTML characters twice when only 1 person awarded', () => {
awardsMock = [...new Array(1)].map(createAwardEmoji);
mountComponent();
awardsMock.forEach(award => {
expect(findTooltip()).toContain(award.user.name);
});
});
it('should not escape special HTML characters twice when 2 people awarded', () => {
awardsMock = [...new Array(2)].map(createAwardEmoji);
mountComponent();
awardsMock.forEach(award => {
expect(findTooltip()).toContain(award.user.name);
});
});
it('should not escape special HTML characters twice when more than 10 people awarded', () => {
awardsMock = [...new Array(11)].map(createAwardEmoji);
mountComponent();
// Testing only the first 10 awards since 11 onward will not be displayed.
awardsMock.slice(0, 10).forEach(award => {
expect(findTooltip()).toContain(award.user.name);
});
});
});
describe('when the user cannot award emoji', () => { describe('when the user cannot award emoji', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(awardsNote); const Component = Vue.extend(awardsNote);
......
...@@ -1530,7 +1530,7 @@ describe Repository do ...@@ -1530,7 +1530,7 @@ describe Repository do
expect(merge_request.reload.rebase_commit_sha).to eq(new_sha) expect(merge_request.reload.rebase_commit_sha).to eq(new_sha)
end end
it 'does rollback when an error is encountered in the second step' do it 'does rollback when a PreReceiveError is encountered in the second step' do
second_response = double(pre_receive_error: 'my_error', git_error: nil) second_response = double(pre_receive_error: 'my_error', git_error: nil)
mock_gitaly(second_response) mock_gitaly(second_response)
...@@ -1541,6 +1541,17 @@ describe Repository do ...@@ -1541,6 +1541,17 @@ describe Repository do
expect(merge_request.reload.rebase_commit_sha).to be_nil expect(merge_request.reload.rebase_commit_sha).to be_nil
end end
it 'does rollback when a GitError is encountered in the second step' do
second_response = double(pre_receive_error: nil, git_error: 'git error')
mock_gitaly(second_response)
expect do
repository.rebase(user, merge_request)
end.to raise_error(Gitlab::Git::Repository::GitError)
expect(merge_request.reload.rebase_commit_sha).to be_nil
end
def mock_gitaly(second_response) def mock_gitaly(second_response)
responses = [ responses = [
double(rebase_sha: new_sha).as_null_object, double(rebase_sha: new_sha).as_null_object,
......
...@@ -11,8 +11,7 @@ describe PersonalSnippetPolicy do ...@@ -11,8 +11,7 @@ describe PersonalSnippetPolicy do
let(:author_permissions) do let(:author_permissions) do
[ [
:update_personal_snippet, :update_personal_snippet,
:admin_personal_snippet, :admin_personal_snippet
:destroy_personal_snippet
] ]
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