Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-10-11 00:06:24 +00:00
parent f607152a08
commit 133924c6cc
30 changed files with 228 additions and 55 deletions

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -99,7 +99,10 @@ export default {
return !groupId ? referencePath.split('#')[0] : null;
},
orderedLabels() {
return _.sortBy(this.issue.labels, 'title');
return _.chain(this.issue.labels)
.filter(this.isNonListLabel)
.sortBy('title')
.value();
},
helpLink() {
return boardsStore.scopedLabels.helpLink;
@ -130,6 +133,9 @@ export default {
if (!label.id) return false;
return true;
},
isNonListLabel(label) {
return label.id && !(this.list.type === 'label' && this.list.title === label.title);
},
filterByLabel(label) {
if (!this.updateFilters) return;
const labelTitle = encodeURIComponent(label.title);
@ -167,7 +173,7 @@ export default {
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<template v-for="label in orderedLabels" v-if="showLabel(label)">
<template v-for="label in orderedLabels">
<issue-card-inner-scoped-label
v-if="showScopedLabel(label)"
:key="label.id"

View File

@ -87,6 +87,14 @@ export function getLocationHash(url = window.location.href) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
}
/**
* Returns a boolean indicating whether the URL hash contains the given string value
*/
export function doesHashExistInUrl(hashName) {
const hash = getLocationHash();
return hash && hash.includes(hashName);
}
/**
* Apply the fragment to the given url by returning a new url string that includes
* the fragment. If the given url already contains a fragment, the original fragment

View File

@ -1,13 +1,14 @@
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
HISTORY_ONLY_FILTER_VALUE,
DISCUSSION_TAB_LABEL,
DISCUSSION_FILTER_TYPES,
NOTE_UNDERSCORE,
} from '../constants';
import notesEventHub from '../event_hub';
@ -28,7 +29,9 @@ export default {
},
data() {
return {
currentValue: this.selectedValue,
currentValue: doesHashExistInUrl(NOTE_UNDERSCORE)
? DISCUSSION_FILTERS_DEFAULT_VALUE
: this.selectedValue,
defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
displayFilters: true,
};
@ -50,7 +53,6 @@ export default {
notesEventHub.$on('dropdownSelect', this.selectFilter);
window.addEventListener('hashchange', this.handleLocationHash);
this.handleLocationHash();
},
mounted() {
this.toggleCommentsForm();

View File

@ -1,7 +1,7 @@
<script>
import { __ } from '~/locale';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import * as constants from '../constants';
import eventHub from '../event_hub';
@ -156,19 +156,17 @@ export default {
this.isFetching = true;
return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
})
return this.fetchDiscussions(this.getFetchDiscussionsConfig())
.then(this.initPolling)
.then(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
this.isFetching = false;
})
.then(() => this.$nextTick())
.then(() => this.startTaskList())
.then(() => this.checkLocationHash())
.then(this.$nextTick)
.then(this.startTaskList)
.then(this.checkLocationHash)
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
@ -199,9 +197,20 @@ export default {
},
startReplying(discussionId) {
return this.convertToDiscussion(discussionId)
.then(() => this.$nextTick())
.then(this.$nextTick)
.then(() => eventHub.$emit('startReplying', discussionId));
},
getFetchDiscussionsConfig() {
const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) {
return Object.assign({}, defaultConfig, {
filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
persistFilter: false,
});
}
return defaultConfig;
},
},
systemNote: constants.SYSTEM_NOTE,
};

View File

@ -8,8 +8,6 @@ export const OPENED = 'opened';
export const REOPENED = 'reopened';
export const CLOSED = 'closed';
export const MERGED = 'merged';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
@ -19,6 +17,7 @@ export const DESCRIPTION_TYPE = 'changed the description';
export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const DISCUSSION_TAB_LABEL = 'show';
export const NOTE_UNDERSCORE = 'note_';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,

View File

@ -28,4 +28,14 @@ module TagsHelper
def protected_tag?(project, tag)
ProtectedTag.protected?(project, tag.name)
end
def tag_description_help_text
text = s_('TagsPage|Optionally, add a message to the tag. Leaving this blank creates '\
'a %{link_start}lightweight tag.%{link_end}') % {
link_start: '<a href="https://git-scm.com/book/en/v2/Git-Basics-Tagging\" target="_blank" rel="noopener noreferrer">',
link_end: '</a>'
}
text.html_safe
end
end

View File

@ -33,9 +33,12 @@ class SlashCommandsService < Service
return unless valid_token?(params[:token])
chat_user = find_chat_user(params)
user = chat_user&.user
if user
unless user.can?(:use_slash_commands)
return Gitlab::SlashCommands::Presenters::Access.new.deactivated if user.deactivated?
if chat_user&.user
unless chat_user.user.can?(:use_slash_commands)
return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project)
end

View File

@ -48,6 +48,7 @@ class GlobalPolicy < BasePolicy
prevent :access_git
prevent :access_api
prevent :receive_notifications
prevent :use_slash_commands
end
rule { required_terms_not_accepted }.policy do

View File

@ -9,6 +9,8 @@
= s_('AdminUsers|The user will not be able to access the API')
%li
= s_('AdminUsers|The user will not receive any notifications')
%li
= s_('AdminUsers|The user will not be able to use slash commands')
%li
= s_('AdminUsers|When the user logs back in, their account will reactivate as a fully active account')
%li

View File

@ -31,7 +31,7 @@
.col-sm-10
= text_area_tag :message, @message, required: false, class: 'form-control', rows: 5
.form-text.text-muted
= s_('TagsPage|Optionally, add a message to the tag.')
= tag_description_help_text
%hr
.form-group.row
= label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2'

View File

@ -6,13 +6,13 @@ class PruneOldEventsWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform
# Contribution calendar shows maximum 12 months of events, we retain 2 years for data integrity.
# Contribution calendar shows maximum 12 months of events, we retain 3 years for data integrity.
# Double nested query is used because MySQL doesn't allow DELETE subqueries on the same table.
Event.unscoped.where(
'(id IN (SELECT id FROM (?) ids_to_remove))',
Event.unscoped.where(
'created_at < ?',
(2.years + 1.day).ago)
(3.years + 1.day).ago)
.select(:id)
.limit(10_000))
.delete_all

View File

@ -0,0 +1,5 @@
---
title: Hide redundant labels in issue boards
merge_request: 17937
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix notes race condition when linking to specific note
merge_request: 17777
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Do not allow deactivated users to use slash commands
merge_request: 18365
author:
type: fixed

View File

@ -666,7 +666,7 @@ build:
CAUTION: **Warning:**
There are some points to be aware of when
[using this feature with new branches or tags *without* pipelines for merge requests](using-onlychanges-without-pipelines-for-merge-requests).
[using this feature with new branches or tags *without* pipelines for merge requests](#using-onlychanges-without-pipelines-for-merge-requests).
##### Using `only:changes` with pipelines for merge requests

View File

@ -55,6 +55,7 @@ A deactivated user:
- Cannot access Git repositories or the API.
- Will not receive any notifications from GitLab.
- Will not be able to use [slash commands](../../../integration/slash_commands.md).
Personal projects, group and user history of the deactivated user will be left intact.

View File

@ -244,6 +244,12 @@ Sign in and re-enable two-factor authentication as soon as possible.
- The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
the U2F key has only been registered on `first.host.xyz`.
## Troubleshooting
If you are receiving an `invalid pin code` error, this may indicate that there is a time sync issue between the authentication application and the GitLab instance itself.
Most authentication apps have a feature in the settings for syncing the time for the codes themselves. For Google Authenticator for example, go to `Settings > Time correction for codes`.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -15,6 +15,15 @@ module Gitlab
MESSAGE
end
def deactivated
ephemeral_response(text: <<~MESSAGE)
You are not allowed to perform the given chatops command since
your account has been deactivated by your administrator.
Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}
MESSAGE
end
def not_found
ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
end

View File

@ -375,6 +375,12 @@ msgstr ""
msgid "%{title} changes"
msgstr ""
msgid "%{total} open issue weight"
msgstr ""
msgid "%{total} open issues"
msgstr ""
msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr ""
@ -1252,6 +1258,9 @@ msgstr ""
msgid "AdminUsers|The user will not be able to access the API"
msgstr ""
msgid "AdminUsers|The user will not be able to use slash commands"
msgstr ""
msgid "AdminUsers|The user will not receive any notifications"
msgstr ""
@ -2649,7 +2658,7 @@ msgstr ""
msgid "Built-in"
msgstr ""
msgid "BurndownChartLabel|Guideline"
msgid "Burndown chart"
msgstr ""
msgid "BurndownChartLabel|Open issue weight"
@ -2658,12 +2667,6 @@ msgstr ""
msgid "BurndownChartLabel|Open issues"
msgstr ""
msgid "BurndownChartLabel|Progress"
msgstr ""
msgid "BurndownChartLabel|Remaining"
msgstr ""
msgid "Business"
msgstr ""
@ -8303,6 +8306,9 @@ msgstr ""
msgid "GroupsTree|Search by name"
msgstr ""
msgid "Guideline"
msgstr ""
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
msgstr ""
@ -8943,6 +8949,9 @@ msgstr ""
msgid "Issue was closed by %{name} %{reason}"
msgstr ""
msgid "Issue weight"
msgstr ""
msgid "IssueBoards|Board"
msgstr ""
@ -15814,7 +15823,7 @@ msgstr ""
msgid "TagsPage|New tag"
msgstr ""
msgid "TagsPage|Optionally, add a message to the tag."
msgid "TagsPage|Optionally, add a message to the tag. Leaving this blank creates a %{link_start}lightweight tag.%{link_end}"
msgstr ""
msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
@ -17062,9 +17071,15 @@ msgstr ""
msgid "Total artifacts size: %{total_size}"
msgstr ""
msgid "Total issues"
msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
msgid "Total weight"
msgstr ""
msgid "Total: %{total}"
msgstr ""

View File

@ -251,7 +251,7 @@ describe 'Issue Boards', :js do
expect(page).to have_selector(selector, text: development.title, count: 1)
end
it 'issue moves between lists' do
it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
drag(list_from_index: 1, from_index: 1, list_to_index: 2)
wait_for_board_cards(2, 7)
@ -259,10 +259,10 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
expect(find('.board:nth-child(3)').all('.board-card').last).to have_content(development.title)
expect(find('.board:nth-child(3)').all('.board-card').last).not_to have_content(development.title)
end
it 'issue moves between lists' do
it 'issue moves between lists and does not show the "Planning" label since the card is in the "Planning" list label' do
drag(list_from_index: 2, list_to_index: 1)
wait_for_board_cards(2, 9)
@ -270,7 +270,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
expect(find('.board:nth-child(2)').all('.board-card').first).to have_content(planning.title)
expect(find('.board:nth-child(2)').all('.board-card').first).not_to have_content(planning.title)
end
it 'issue moves from closed' do

View File

@ -304,7 +304,8 @@ describe 'Issue Boards', :js do
end
end
expect(card).to have_selector('.badge', count: 3)
# 'Development' label does not show since the card is in a 'Development' list label
expect(card).to have_selector('.badge', count: 2)
expect(card).to have_content(bug.title)
end
@ -330,7 +331,8 @@ describe 'Issue Boards', :js do
end
end
expect(card).to have_selector('.badge', count: 4)
# 'Development' label does not show since the card is in a 'Development' list label
expect(card).to have_selector('.badge', count: 3)
expect(card).to have_content(bug.title)
expect(card).to have_content(regression.title)
end
@ -357,7 +359,8 @@ describe 'Issue Boards', :js do
end
end
expect(card).to have_selector('.badge', count: 1)
# 'Development' label does not show since the card is in a 'Development' list label
expect(card).to have_selector('.badge', count: 0)
expect(card).not_to have_content(stretch.title)
end

View File

@ -136,6 +136,24 @@ describe('URL utility', () => {
});
});
describe('doesHashExistInUrl', () => {
it('should return true when the given string exists in the URL hash', () => {
setWindowLocation({
href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
});
expect(urlUtils.doesHashExistInUrl('note_')).toBe(true);
});
it('should return false when the given string does not exist in the URL hash', () => {
setWindowLocation({
href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
});
expect(urlUtils.doesHashExistInUrl('doesnotexist')).toBe(false);
});
});
describe('setUrlFragment', () => {
it('should set fragment when url has no fragment', () => {
const url = urlUtils.setUrlFragment('/home/feature', 'usage');

View File

@ -32,7 +32,10 @@ describe('Issue card component', () => {
beforeEach(() => {
setFixtures('<div class="test-container"></div>');
list = listObj;
list = {
...listObj,
type: 'label',
};
issue = new ListIssue({
title: 'Testing',
id: 1,
@ -241,8 +244,8 @@ describe('Issue card component', () => {
Vue.nextTick(() => done());
});
it('renders list label', () => {
expect(component.$el.querySelectorAll('.badge').length).toBe(2);
it('does not render list label but renders all other labels', () => {
expect(component.$el.querySelectorAll('.badge').length).toBe(1);
});
it('renders label', () => {
@ -278,7 +281,7 @@ describe('Issue card component', () => {
Vue.nextTick()
.then(() => {
expect(component.$el.querySelectorAll('.badge').length).toBe(2);
expect(component.$el.querySelectorAll('.badge').length).toBe(1);
expect(component.$el.textContent).not.toContain('closed');
done();

View File

@ -15,7 +15,7 @@ export const listObj = {
weight: 3,
label: {
id: 5000,
title: 'Testing',
title: 'Test',
color: 'red',
description: 'testing;',
textColor: 'white',
@ -30,7 +30,7 @@ export const listObjDuplicate = {
weight: 3,
label: {
id: listObj.label.id,
title: 'Testing',
title: 'Test',
color: 'red',
description: 'testing;',
},

View File

@ -160,5 +160,28 @@ describe('DiscussionFilter component', () => {
done();
});
});
it('fetches discussions when there is a hash', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.currentValue = discussionFiltersMock[2].value;
spyOn(vm, 'selectFilter');
vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.selectFilter).toHaveBeenCalled();
done();
});
});
it('does not fetch discussions when there is no hash', done => {
window.location.hash = '';
spyOn(vm, 'selectFilter');
vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.selectFilter).not.toHaveBeenCalled();
done();
});
});
});
});

View File

@ -3,6 +3,13 @@
require 'spec_helper'
describe Gitlab::SlashCommands::Presenters::Access do
shared_examples_for 'displays an error message' do
it do
expect(subject[:text]).to match(error_message)
expect(subject[:response_type]).to be(:ephemeral)
end
end
describe '#access_denied' do
let(:project) { build(:project) }
@ -10,9 +17,18 @@ describe Gitlab::SlashCommands::Presenters::Access do
it { is_expected.to be_a(Hash) }
it 'displays an error message' do
expect(subject[:text]).to match('are not allowed')
expect(subject[:response_type]).to be(:ephemeral)
it_behaves_like 'displays an error message' do
let(:error_message) { 'you do not have access to the GitLab project' }
end
end
describe '#deactivated' do
subject { described_class.new.deactivated }
it { is_expected.to be_a(Hash) }
it_behaves_like 'displays an error message' do
let(:error_message) { 'your account has been deactivated by your administrator' }
end
end

View File

@ -288,6 +288,14 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:use_slash_commands) }
end
context 'when deactivated' do
before do
current_user.deactivate
end
it { is_expected.not_to be_allowed(:use_slash_commands) }
end
context 'when access locked' do
before do
current_user.lock_access!

View File

@ -94,16 +94,32 @@ RSpec.shared_examples 'chat slash commands service' do
subject.trigger(params)
end
shared_examples_for 'blocks command execution' do
it do
expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
result = subject.trigger(params)
expect(result[:text]).to match(error_message)
end
end
context 'when user is blocked' do
before do
chat_name.user.block
end
it 'blocks command execution' do
expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
it_behaves_like 'blocks command execution' do
let(:error_message) { 'you do not have access to the GitLab project' }
end
end
result = subject.trigger(params)
expect(result).to include(text: /^You are not allowed/)
context 'when user is deactivated' do
before do
chat_name.user.deactivate
end
it_behaves_like 'blocks command execution' do
let(:error_message) { 'your account has been deactivated by your administrator' }
end
end
end

View File

@ -6,12 +6,12 @@ describe PruneOldEventsWorker do
describe '#perform' do
let(:user) { create(:user) }
let!(:expired_event) { create(:event, :closed, author: user, created_at: 25.months.ago) }
let!(:expired_event) { create(:event, :closed, author: user, created_at: 37.months.ago) }
let!(:not_expired_1_day_event) { create(:event, :closed, author: user, created_at: 1.day.ago) }
let!(:not_expired_13_month_event) { create(:event, :closed, author: user, created_at: 13.months.ago) }
let!(:not_expired_2_years_event) { create(:event, :closed, author: user, created_at: 2.years.ago) }
let!(:not_expired_3_years_event) { create(:event, :closed, author: user, created_at: 3.years.ago) }
it 'prunes events older than 2 years' do
it 'prunes events older than 3 years' do
expect { subject.perform }.to change { Event.count }.by(-1)
expect(Event.find_by(id: expired_event.id)).to be_nil
end
@ -26,9 +26,9 @@ describe PruneOldEventsWorker do
expect(not_expired_13_month_event.reload).to be_present
end
it 'leaves events from 2 years ago' do
it 'leaves events from 3 years ago' do
subject.perform
expect(not_expired_2_years_event).to be_present
expect(not_expired_3_years_event).to be_present
end
end
end