diff options
135 files changed, 2110 insertions, 656 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 97e39ce99cb..77ad4753c84 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -268,6 +268,7 @@ Rails/Presence: - 'app/models/clusters/platforms/kubernetes.rb' - 'app/models/concerns/mentionable.rb' - 'app/models/concerns/token_authenticatable.rb' + - 'app/models/project_services/hipchat_service.rb' - 'app/models/project_services/irker_service.rb' - 'app/models/project_services/jira_service.rb' - 'app/models/project_services/kubernetes_service.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e91b9f29ff..41506746c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.9.8 (2019-04-11) + +### Deprecated (1 change) + +- Allow to use untrusted Regexp via feature flag. !26905 + +### Performance (2 changes) + +- Improve performance of PR import. !27121 +- Disable method instrumentation for diffs. !27235 + +### Other (1 change) + +- Restore HipChat project service. !27172 + + ## 11.9.7 (2019-04-09) - No changes. diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index f9c71a52e2f..acd405b1d62 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.5.1 +8.6.0 @@ -201,6 +201,9 @@ gem 'connection_pool', '~> 2.0' # Discord integration gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false +# HipChat integration +gem 'hipchat', '~> 1.5.0' + # JIRA integration gem 'jira-ruby', '~> 1.4' @@ -342,14 +345,14 @@ group :development, :test do # Generate Fake data gem 'ffaker', '~> 2.10' - gem 'capybara', '~> 2.16.1' - gem 'capybara-screenshot', '~> 1.0.18' - gem 'selenium-webdriver', '~> 3.12' + gem 'capybara', '~> 2.18.0' + gem 'capybara-screenshot', '~> 1.0.22' + gem 'selenium-webdriver', '~> 3.141' gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'gitlab-styles', '~> 2.4', require: false + gem 'gitlab-styles', '~> 2.5', require: false # Pin these dependencies, otherwise a new rule could break the CI pipelines gem 'rubocop', '~> 0.54.0' gem 'rubocop-rspec', '~> 1.22.1' diff --git a/Gemfile.lock b/Gemfile.lock index b522aa85b39..c1d1a50799f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,13 +100,13 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (9.1.0) - capybara (2.16.1) + capybara (2.18.0) addressable mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - xpath (~> 2.0) + xpath (>= 2.0, < 4.0) capybara-screenshot (1.0.22) capybara (>= 1.0, < 4) launchy @@ -289,7 +289,7 @@ GEM gitlab-markup (1.7.0) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) - gitlab-styles (2.5.1) + gitlab-styles (2.5.2) rubocop (~> 0.54.0) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) @@ -366,6 +366,9 @@ GEM hashie (>= 3.0) health_check (2.6.0) rails (>= 4.0) + hipchat (1.5.2) + httparty + mimemagic html-pipeline (2.8.4) activesupport (>= 2) nokogiri (>= 1.4) @@ -485,7 +488,7 @@ GEM net-ssh (5.0.1) netrc (0.11.0) nio4r (2.3.1) - nokogiri (1.10.1) + nokogiri (1.10.2) mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri @@ -703,7 +706,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.6.0) redis (>= 2.2, < 5) - regexp_parser (1.3.0) + regexp_parser (1.4.0) regexp_property_values (0.3.4) representable (3.0.4) declarative (< 0.1.0) @@ -812,9 +815,9 @@ GEM seed-fu (2.3.7) activerecord (>= 3.1) activesupport (>= 3.1) - selenium-webdriver (3.12.0) + selenium-webdriver (3.141.0) childprocess (~> 0.5) - rubyzip (~> 1.2) + rubyzip (~> 1.2, >= 1.2.2) sentry-raven (2.9.0) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) @@ -940,8 +943,8 @@ GEM rinku with_env (1.1.0) xml-simple (1.1.5) - xpath (2.1.0) - nokogiri (~> 1.3) + xpath (3.2.0) + nokogiri (~> 1.8) PLATFORMS ruby @@ -971,8 +974,8 @@ DEPENDENCIES browser (~> 2.5) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) - capybara (~> 2.16.1) - capybara-screenshot (~> 1.0.18) + capybara (~> 2.18.0) + capybara-screenshot (~> 1.0.22) carrierwave (~> 1.3) charlock_holmes (~> 0.7.5) chronic (~> 0.10.2) @@ -1022,7 +1025,7 @@ DEPENDENCIES gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.7.0) gitlab-sidekiq-fetcher (~> 0.4.0) - gitlab-styles (~> 2.4) + gitlab-styles (~> 2.5) gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.2) google-api-client (~> 0.23) @@ -1040,6 +1043,7 @@ DEPENDENCIES hangouts-chat (~> 0.0.5) hashie-forbidden_attributes health_check (~> 2.6.0) + hipchat (~> 1.5.0) html-pipeline (~> 2.8) html2text httparty (~> 0.16.4) @@ -1142,7 +1146,7 @@ DEPENDENCIES sass-rails (~> 5.0.6) scss_lint (~> 0.56.0) seed-fu (~> 2.3.7) - selenium-webdriver (~> 3.12) + selenium-webdriver (~> 3.141) sentry-raven (~> 2.7) settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js index 17d15278a74..6f81d6bc6f8 100644 --- a/app/assets/javascripts/boards/models/milestone.js +++ b/app/assets/javascripts/boards/models/milestone.js @@ -1,7 +1,16 @@ -class ListMilestone { +import { isEE } from '~/lib/utils/common_utils'; + +export default class ListMilestone { constructor(obj) { this.id = obj.id; this.title = obj.title; + + if (isEE) { + this.path = obj.path; + this.state = obj.state; + this.webUrl = obj.web_url || obj.webUrl; + this.description = obj.description; + } } } diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 5e74998579b..0ed4dcdcd81 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -64,6 +64,11 @@ export default { required: false, default: '', }, + isFluidLayout: { + type: Boolean, + required: false, + default: false, + }, }, data() { const treeWidth = @@ -116,7 +121,7 @@ export default { return this.treeWidth <= TREE_HIDE_STATS_WIDTH; }, isLimitedContainer() { - return !this.showTreeList && !this.isParallelView; + return !this.showTreeList && !this.isParallelView && !this.isFluidLayout; }, }, watch: { diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index 69146f1f6fd..1faa0493e79 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -41,7 +41,7 @@ export default { <template> <tr v-if="shouldRender" :class="className" class="notes_holder"> - <td class="notes_content" colspan="3"> + <td class="notes-content" colspan="3"> <div class="content"> <diff-discussions v-if="line.discussions.length" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index 370cb6e339a..d2e54edca85 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -87,7 +87,7 @@ export default { <template> <tr v-if="shouldRender" :class="className" class="notes_holder"> - <td class="notes_content parallel old" colspan="2"> + <td class="notes-content parallel old" colspan="2"> <div v-if="shouldRenderDiscussionsOnLeft" class="content"> <diff-discussions v-if="line.left.discussions.length" @@ -105,7 +105,7 @@ export default { line-position="left" /> </td> - <td class="notes_content parallel new" colspan="2"> + <td class="notes-content parallel new" colspan="2"> <div v-if="shouldRenderDiscussionsOnRight" class="content"> <diff-discussions v-if="line.right.discussions.length" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 5dabe224baa..4feb73cfef2 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -49,4 +49,4 @@ export const TYPE_KEY = 'type'; export const LEFT_LINE_KEY = 'left'; export const CENTERED_LIMITED_CONTAINER_CLASSES = - 'container-limited limit-container-width mx-auto px-3'; + 'container-limited limit-container-width mx-lg-auto px-3'; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 63954d9d412..1d897bca1dd 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -71,6 +71,7 @@ export default function initDiffsApp(store) { helpPagePath: dataset.helpPagePath, currentUser: JSON.parse(dataset.currentUserData) || {}, changesEmptyStateIllustration: dataset.changesEmptyStateIllustration, + isFluidLayout: parseBoolean(dataset.isFluidLayout), }; }, computed: { @@ -97,6 +98,7 @@ export default function initDiffsApp(store) { helpPagePath: this.helpPagePath, shouldShow: this.activeTab === 'diffs', changesEmptyStateIllustration: this.changesEmptyStateIllustration, + isFluidLayout: this.isFluidLayout, }, }); }, diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 00e41dd0301..765969daa32 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import $ from 'jquery'; class DirtySubmitForm { constructor(form) { @@ -26,6 +27,7 @@ class DirtySubmitForm { ); this.form.addEventListener('input', throttledUpdateDirtyInput); this.form.addEventListener('change', throttledUpdateDirtyInput); + $(this.form).on('change.select2', throttledUpdateDirtyInput); this.form.addEventListener('submit', event => this.formSubmit(event)); } diff --git a/app/assets/javascripts/event_tracking/notes.js b/app/assets/javascripts/event_tracking/notes.js new file mode 100644 index 00000000000..2d1ec238274 --- /dev/null +++ b/app/assets/javascripts/event_tracking/notes.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 0670e2b06b9..7594edfac27 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -275,7 +275,7 @@ export default { <!-- job log --> <div v-if="hasTrace" - class="build-trace-container" + class="build-trace-container position-relative" :class="{ 'prepend-top-default': !job.archived }" > <log-top-bar diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 845699a90b5..a55dffbe488 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -43,7 +43,7 @@ export default { <template> <div - class="build-job" + class="build-job position-relative" :class="{ retried: job.retried, active: isActive, @@ -56,7 +56,11 @@ export default { data-boundary="viewport" class="js-job-link" > - <icon v-if="isActive" name="arrow-right" class="js-arrow-right icon-arrow-right" /> + <icon + v-if="isActive" + name="arrow-right" + class="js-arrow-right icon-arrow-right position-absolute d-block" + /> <ci-icon :status="job.status" /> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 52e14f954ee..607b2bd1c74 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -75,7 +75,11 @@ export default { <template v-if="isTraceSizeVisible"> {{ jobLogSize }} - <gl-link v-if="rawPath" :href="rawPath" class="js-raw-link raw-link"> + <gl-link + v-if="rawPath" + :href="rawPath" + class="js-raw-link text-plain text-underline prepend-left-5" + > {{ s__('Job|Complete Raw') }} </gl-link> </template> diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue index 997737b3e23..922f64d93fe 100644 --- a/app/assets/javascripts/jobs/components/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -52,7 +52,7 @@ export default { </p> <template v-if="hasVariables"> - <p class="trigger-variables-btn-container"> + <p class="trigger-variables-btn-container d-flex"> <span class="font-weight-bold">{{ __('Trigger variables:') }}</span> <gl-button diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 1b7f8732c65..cc1d85fd97d 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -44,18 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : export const dasherize = str => str.replace(/[_\s]+/g, '-'); /** - * Removes accents and converts to lower case + * Replaces whitespaces with hyphens and converts to lower case * @param {String} str * @returns {String} */ -export const slugify = str => str.trim().toLowerCase(); +export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-'); /** - * Replaces whitespaces with hyphens and converts to lower case + * Replaces whitespaces with underscore and converts to lower case * @param {String} str * @returns {String} */ -export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-'); +export const slugifyWithUnderscore = str => str.toLowerCase().replace(/\s+/g, '_'); /** * Truncates given text diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 2f15da42271..509f19e6f00 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -398,7 +398,7 @@ export default class MergeRequestTabs { const hash = getLocationHash(); const anchor = hash && $container.find(`.note[id="${hash}"]`); if (anchor && anchor.length > 0) { - const notesContent = anchor.closest('.notes_content'); + const notesContent = anchor.closest('.notes-content'); const lineType = notesContent.hasClass('new') ? 'new' : 'old'; Notes.instance.toggleDiffNote({ target: anchor, diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js index cc686b401d2..9a97e98f9db 100644 --- a/app/assets/javascripts/mr_popover/index.js +++ b/app/assets/javascripts/mr_popover/index.js @@ -54,9 +54,13 @@ export default elements => { const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); + const listenerAddedAttr = 'data-mr-listener-added'; mrLinks.forEach(el => { - el.addEventListener('mouseenter', handleMRPopoverMount(apolloProvider)); + if (!el.getAttribute(listenerAddedAttr)) { + el.addEventListener('mouseenter', handleMRPopoverMount(apolloProvider)); + el.setAttribute(listenerAddedAttr, true); + } }); } }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 94d2e2b53e9..36725e22365 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -506,7 +506,7 @@ export default class Notes { var contentContainerClass = '.' + $notes - .closest('.notes_content') + .closest('.notes-content') .attr('class') .split(' ') .join('.'); @@ -1069,14 +1069,14 @@ export default class Notes { addForm = false; let lineTypeSelector = ''; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_content" colspan="3"><div class="content"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes-content" colspan="3"><div class="content"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes-content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes-content parallel new"><div class="content"></div></td></tr>'; } - const notesContentSelector = `.notes_content${lineTypeSelector} .content`; + const notesContentSelector = `.notes-content${lineTypeSelector} .content`; let notesContent = targetRow.find(notesContentSelector); if (hasNotes && showReplyInput) { diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index b30d7fa9b73..caf22f71bf9 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -11,6 +11,7 @@ import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase, + slugifyWithUnderscore, } from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; @@ -129,6 +130,9 @@ export default { ? 'merge request' : 'issue'; }, + trackingLabel() { + return slugifyWithUnderscore(`${this.commentButtonTitle} button`); + }, }, watch: { note(newNote) { @@ -370,6 +374,8 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" type="submit" + :data-track-label="trackingLabel" + data-track-event="click_button" @click.prevent="handleSave()" > {{ __(commentButtonTitle) }} diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index ab758a9e922..b95835ed10a 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -105,7 +105,7 @@ export default { </td> </tr> <tr class="notes_holder"> - <td class="notes_content" colspan="3"><slot></slot></td> + <td class="notes-content" colspan="3"><slot></slot></td> </tr> </table> </div> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 30372103590..57dd1c5cab2 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import { isEE } from '~/lib/utils/common_utils'; +import initNoteStats from 'ee_else_ce/event_tracking/notes'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; import createStore from './stores'; @@ -38,6 +40,11 @@ document.addEventListener('DOMContentLoaded', () => { notesData: JSON.parse(notesDataset.notesData), }; }, + mounted() { + if (isEE) { + initNoteStats(); + } + }, render(createElement) { return createElement('notes-app', { props: { diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 278c35d3846..92ed6a652d7 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -3,17 +3,24 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; +import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initAvatarPicker from '~/avatar_picker'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectPermissionsSettings from '../shared/permissions'; document.addEventListener('DOMContentLoaded', () => { - initProjectLoadingSpinner(); - setupProjectEdit(); - // Initialize expandable settings panels - initSettingsPanels(); initAvatarPicker(); - initProjectPermissionsSettings(); initConfirmDangerModal(); + initSettingsPanels(); mountBadgeSettings(PROJECT_BADGE); + + initProjectLoadingSpinner(); + initProjectPermissionsSettings(); + setupProjectEdit(); + + dirtySubmitFactory( + document.querySelectorAll( + '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form', + ), + ); }); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c3031c3375e..be0073559b6 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -188,11 +188,6 @@ li.note { background-color: inherit; } -.show-suppressed-diff, -.show-all-commits { - cursor: pointer; -} - .error-message { padding: 10px; background: $red-400; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 1c23c14c2de..be544c0a814 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -157,6 +157,10 @@ label { padding-left: 10px; padding-right: 10px; appearance: none; + /* stylelint-disable property-no-vendor-prefix */ + -webkit-appearance: none; + -moz-appearance: none; + /* stylelint-enable property-no-vendor-prefix */ &::-ms-expand { display: none; diff --git a/app/assets/stylesheets/framework/notes.scss b/app/assets/stylesheets/framework/notes.scss index d349e3fad9c..85ddf11d6fe 100644 --- a/app/assets/stylesheets/framework/notes.scss +++ b/app/assets/stylesheets/framework/notes.scss @@ -4,7 +4,7 @@ } // Diff is side by side - .notes_content.parallel & { + .notes-content.parallel & { // We hide at double what we normally hide at because // there are two columns of notes @media (#{$condition}-width: (2 * $breakpoint-width)) { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 916f6cd3137..6fc742871e7 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -46,10 +46,6 @@ } .build-page { - .build-trace-container { - position: relative; - } - .build-trace { @include build-trace(); } @@ -104,18 +100,6 @@ top: 0; } - .truncated-info { - .truncated-info-size { - margin: 0 5px; - } - - .raw-link { - color: $gl-text-color; - margin-left: 5px; - text-decoration: underline; - } - } - .controllers { @include build-controllers(15px, center, false, 0, inline, 0); } @@ -142,12 +126,6 @@ } } -.with-performance-bar .build-page { - .top-bar.affix { - top: $header-height + $performance-bar-height; - } -} - .build-header { .ci-header-container, .header-action-buttons { @@ -233,7 +211,6 @@ } .trigger-variables-btn-container { - @extend .d-flex; justify-content: space-between; align-items: center; @@ -277,12 +254,6 @@ .retry-link { display: block; - .btn { - i { - margin-left: 5px; - } - } - .btn-inverted-secondary { color: $blue-500; @@ -329,16 +300,12 @@ } } - .build-job { - position: relative; - - .icon-arrow-right { - position: absolute; - left: 15px; - top: 20px; - display: block; - } + .icon-arrow-right { + left: 15px; + top: 20px; + } + .build-job { &.active { font-weight: $gl-font-weight-bold; } @@ -350,10 +317,6 @@ &:hover { background-color: $gray-darker; } - - .icon-retry { - margin-left: 3px; - } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9be3f8138a0..fbd291f095a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,8 +1,3 @@ -// Limit MR description for side-by-side diff view -.fixed-width-container { - @include fixed-width-container; -} - .issuable-warning-icon { background-color: $orange-100; border-radius: $border-radius-default; @@ -27,7 +22,7 @@ .files-changed-inner, .limited-header-width, .limited-width-notes { - @extend .fixed-width-container; + @include fixed-width-container; } .issuable-details { @@ -35,13 +30,13 @@ .mr-source-target, .mr-state-widget, .merge-manually { - @extend .fixed-width-container; + @include fixed-width-container; } } .merge-request-details { .emoji-list-container { - @extend .fixed-width-container; + @include fixed-width-container; } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index fd07415a52f..09f75cd827f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -68,7 +68,7 @@ $note-form-margin-left: 72px; } } - .notes_content { + .notes-content { border: 0; border-top: 1px solid $border-color; } @@ -453,7 +453,7 @@ $note-form-margin-left: 72px; // Merge request notes in diffs // Diff is inline - .notes_content .note-header .note-headline-light { + .notes-content .note-header .note-headline-light { display: inline-block; position: relative; } @@ -465,7 +465,7 @@ $note-form-margin-left: 72px; border: 1px solid $border-color; border-left: 0; - &.notes_content { + &.notes-content { border-width: 1px 0; padding: 0; vertical-align: top; @@ -512,7 +512,7 @@ $note-form-margin-left: 72px; } .commit-diff { - .notes_content { + .notes-content { background-color: $white-light; } } diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 617b3db2fae..85c4902eee2 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -1,9 +1,4 @@ -.js-pipeline-schedule-form { - .dropdown-select, - .dropdown-menu-toggle { - width: 100% !important; - } - +.pipeline-schedule-form { .gl-field-error { margin: 10px 0 0; } diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index f7619ccbd20..94da72622af 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -52,11 +52,6 @@ .report-block-list-icon .loading-container { position: relative; left: -2px; - // needed to make the next element align with the - // elements below that have a svg with 16px width - .fa-spinner { - width: 16px; - } } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 7b0538dca20..0a9c56f5625 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -39,7 +39,7 @@ .settings-header { position: relative; - padding: 20px 110px 10px 0; + padding: 20px 110px 0 0; h4 { margin-top: 0; diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index eb6ddaac871..2f9b4c4eaa2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -198,7 +198,7 @@ class ApplicationSetting < ApplicationRecord validates_each :restricted_visibility_levels do |record, attr, value| value&.each do |level| unless Gitlab::VisibilityLevel.options.value?(level) - record.errors.add(attr, "'#{level}' is not a valid visibility level") + record.errors.add(attr, _("'%{level}' is not a valid visibility level") % { level: level }) end end end @@ -206,7 +206,7 @@ class ApplicationSetting < ApplicationRecord validates_each :import_sources do |record, attr, value| value&.each do |source| unless Gitlab::ImportSources.options.value?(source) - record.errors.add(attr, "'#{source}' is not a import source") + record.errors.add(attr, _("'%{source}' is not a import source") % { source: source }) end end end diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb index 05cd4265133..cfffd845e43 100644 --- a/app/models/concerns/group_descendant.rb +++ b/app/models/concerns/group_descendant.rb @@ -22,7 +22,7 @@ module GroupDescendant return [] if descendants.empty? unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) } - raise ArgumentError.new('element is not a hierarchy') + raise ArgumentError.new(_('element is not a hierarchy')) end all_hierarchies = descendants.map do |descendant| @@ -56,7 +56,7 @@ module GroupDescendant end if parent.nil? && hierarchy_top.present? - raise ArgumentError.new('specified top is not part of the tree') + raise ArgumentError.new(_('specified top is not part of the tree')) end if parent && parent != hierarchy_top diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index df14e6e4754..aafd0b538a3 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -41,7 +41,7 @@ module TokenAuthenticatableStrategies def self.fabricate(model, field, options) if options[:digest] && options[:encrypted] - raise ArgumentError, 'Incompatible options set!' + raise ArgumentError, _('Incompatible options set!') end if options[:digest] diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb index 2c7fa2c5b3c..4728cb658dc 100644 --- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -13,7 +13,7 @@ module TokenAuthenticatableStrategies elsif migrating? find_by_plaintext_token(token, unscoped) else - raise ArgumentError, "Unknown encryption strategy: #{encrypted_strategy}!" + raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy } end end @@ -32,7 +32,7 @@ module TokenAuthenticatableStrategies return super if instance.has_attribute?(encrypted_field) if required? - raise ArgumentError, 'Using required encryption strategy when encrypted field is missing!' + raise ArgumentError, _('Using required encryption strategy when encrypted field is missing!') else insecure_strategy.ensure_token(instance) end @@ -74,7 +74,7 @@ module TokenAuthenticatableStrategies value = value.call if value.is_a?(Proc) unless value.in?([:required, :optional, :migrating]) - raise ArgumentError, 'encrypted: needs to be a :required, :optional or :migrating!' + raise ArgumentError, _('encrypted: needs to be a :required, :optional or :migrating!') end value diff --git a/app/models/milestone.rb b/app/models/milestone.rb index b4aad9e512e..787600569fa 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -291,22 +291,22 @@ class Milestone < ApplicationRecord end title_exists = relation.find_by_title(title) - errors.add(:title, "already being used for another group or project milestone.") if title_exists + errors.add(:title, _("already being used for another group or project milestone.")) if title_exists end # Milestone should be either a project milestone or a group milestone def milestone_type_check if group_id && project_id field = project_id_changed? ? :project_id : :group_id - errors.add(field, "milestone should belong either to a project or a group.") + errors.add(field, _("milestone should belong either to a project or a group.")) end end def milestone_format_reference(format = :iid) - raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) + raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format) if group_milestone? && format == :iid - raise ArgumentError, 'Cannot refer to a group milestone by an internal id!' + raise ArgumentError, _('Cannot refer to a group milestone by an internal id!') end if format == :name && !name.include?('"') @@ -322,7 +322,7 @@ class Milestone < ApplicationRecord def start_date_should_be_less_than_due_date if due_date <= start_date - errors.add(:due_date, "must be greater than start date") + errors.add(:due_date, _("must be greater than start date")) end end diff --git a/app/models/project.rb b/app/models/project.rb index 89ad90a964e..2fb6f5cb6a7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -146,6 +146,7 @@ class Project < ApplicationRecord has_one :pipelines_email_service has_one :irker_service has_one :pivotaltracker_service + has_one :hipchat_service has_one :flowdock_service has_one :assembla_service has_one :asana_service @@ -312,7 +313,7 @@ class Project < ApplicationRecord validates :description, length: { maximum: 2000 }, allow_blank: true validates :ci_config_path, format: { without: %r{(\.{2}|\A/)}, - message: 'cannot include leading slash or directory traversal.' }, + message: _('cannot include leading slash or directory traversal.') }, length: { maximum: 255 }, allow_blank: true validates :name, @@ -419,13 +420,13 @@ class Project < ApplicationRecord enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, - default: 3600, error_message: 'Maximum job timeout has a value which could not be accepted' + default: 3600, error_message: _('Maximum job timeout has a value which could not be accepted') validates :build_timeout, allow_nil: true, numericality: { greater_than_or_equal_to: 10.minutes, less_than: 1.month, only_integer: true, - message: 'needs to be beetween 10 minutes and 1 month' } + message: _('needs to be beetween 10 minutes and 1 month') } # Used by Projects::CleanupService to hold a map of rewritten object IDs mount_uploader :bfg_object_map, AttachmentUploader @@ -849,7 +850,7 @@ class Project < ApplicationRecord def mark_stuck_remote_mirrors_as_failed! remote_mirrors.stuck.update_all( update_status: :failed, - last_error: 'The remote mirror took to long to complete.', + last_error: _('The remote mirror took to long to complete.'), last_update_at: Time.now ) end @@ -886,14 +887,14 @@ class Project < ApplicationRecord level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase - self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.") + self.errors.add(:visibility_level, _("%{level_name} is not allowed in a %{group_level_name} group.") % { level_name: level_name, group_level_name: group_level_name }) end def visibility_level_allowed_as_fork return if visibility_level_allowed_as_fork? level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase - self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.") + self.errors.add(:visibility_level, _("%{level_name} is not allowed since the fork source project has lower visibility.") % { level_name: level_name }) end def check_wiki_path_conflict @@ -902,7 +903,7 @@ class Project < ApplicationRecord path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki" if Project.where(namespace_id: namespace_id, path: path_to_check).exists? - errors.add(:name, 'has already been taken') + errors.add(:name, _('has already been taken')) end end @@ -922,7 +923,7 @@ class Project < ApplicationRecord return unless pages_https_only? unless pages_domains.all?(&:https?) - errors.add(:pages_https_only, "cannot be enabled unless all domains have TLS certificates") + errors.add(:pages_https_only, _("cannot be enabled unless all domains have TLS certificates")) end end @@ -1202,7 +1203,7 @@ class Project < ApplicationRecord def valid_repo? repository.exists? rescue - errors.add(:path, 'Invalid repository path') + errors.add(:path, _('Invalid repository path')) false end @@ -1293,7 +1294,7 @@ class Project < ApplicationRecord # Check if repository with same path already exists on disk we can # skip this for the hashed storage because the path does not change if legacy_storage? && repository_with_same_path_already_exists? - errors.add(:base, 'There is already a repository with that name on disk') + errors.add(:base, _('There is already a repository with that name on disk')) return false end @@ -1315,7 +1316,7 @@ class Project < ApplicationRecord repository.after_create true else - errors.add(:base, 'Failed to create repository via gitlab-shell') + errors.add(:base, _('Failed to create repository via gitlab-shell')) false end end @@ -1391,7 +1392,7 @@ class Project < ApplicationRecord ProjectCacheWorker.perform_async(self.id, [], [:commit_count]) reload_default_branch else - errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist") + errors.add(:base, _("Could not change HEAD: branch '%{branch}' does not exist") % { branch: branch }) false end end @@ -1443,7 +1444,7 @@ class Project < ApplicationRecord ProjectWiki.new(self, self.owner).wiki true rescue ProjectWiki::CouldNotCreateWikiError - errors.add(:base, 'Failed create wiki') + errors.add(:base, _('Failed create wiki')) false end @@ -1932,7 +1933,7 @@ class Project < ApplicationRecord # # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments) def hashed_storage?(feature) - raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature) + raise ArgumentError, _("Invalid feature") unless HASHED_STORAGE_FEATURES.include?(feature) self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature] end @@ -2164,7 +2165,7 @@ class Project < ApplicationRecord return if skip_disk_validation if repository_storage.blank? || repository_with_same_path_already_exists? - errors.add(:base, 'There is already a repository with that name on disk') + errors.add(:base, _('There is already a repository with that name on disk')) throw :abort end end @@ -2210,7 +2211,7 @@ class Project < ApplicationRecord errors.delete(error) end - errors.add(:base, "The project is still being deleted. Please try again later.") + errors.add(:base, _("The project is still being deleted. Please try again later.")) end def pending_delete_twin diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 58b555c3581..feaf172d48d 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -14,7 +14,7 @@ class ProjectGroupLink < ApplicationRecord validates :project_id, presence: true validates :group, presence: true - validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" } + validates :group_id, uniqueness: { scope: [:project_id], message: _("already shared with this group") } validates :group_access, presence: true validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true validate :different_group @@ -44,7 +44,7 @@ class ProjectGroupLink < ApplicationRecord group_ids = project_group.ancestors.map(&:id).push(project_group.id) if group_ids.include?(self.group.id) - errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.") + errors.add(:base, _("Project cannot be shared with the group it is in or one of its ancestors.")) end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index cc5f1207653..3e28dc23782 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -11,7 +11,7 @@ class AsanaService < Service end def description - 'Asana - Teamwork without email' + s_('AsanaService|Asana - Teamwork without email') end def help @@ -36,13 +36,13 @@ http://app.asana.com/-/account_api' { type: 'text', name: 'api_key', - placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.', + placeholder: s_('AsanaService|User Personal Access Token. User must have access to task, all comments will be attributed to this user.'), required: true }, { type: 'text', name: 'restrict_to_branch', - placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + placeholder: s_('AsanaService|Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.') } ] end @@ -73,7 +73,7 @@ http://app.asana.com/-/account_api' project_name = project.full_name data[:commits].each do |commit| - push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):" + push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] } check_commit(commit[:message], push_msg) end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 71f5607dbdb..dfeb21680a9 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -31,15 +31,15 @@ class BambooService < CiService end def title - 'Atlassian Bamboo CI' + s_('BambooService|Atlassian Bamboo CI') end def description - 'A continuous integration and build server' + s_('BambooService|A continuous integration and build server') end def help - 'You must set up automatic revision labeling and a repository trigger in Bamboo.' + s_('BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo.') end def self.to_param @@ -49,11 +49,11 @@ class BambooService < CiService def fields [ { type: 'text', name: 'bamboo_url', - placeholder: 'Bamboo root URL like https://bamboo.example.com', required: true }, + placeholder: s_('BambooService|Bamboo root URL like https://bamboo.example.com'), required: true }, { type: 'text', name: 'build_key', - placeholder: 'Bamboo build plan key like KEY', required: true }, + placeholder: s_('BambooService|Bamboo build plan key like KEY'), required: true }, { type: 'text', name: 'username', - placeholder: 'A user with API access, if applicable' }, + placeholder: s_('BambooService|A user with API access, if applicable') }, { type: 'password', name: 'password' } ] end diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb index 21afd14dbff..405676792de 100644 --- a/app/models/project_services/discord_service.rb +++ b/app/models/project_services/discord_service.rb @@ -4,11 +4,11 @@ require "discordrb/webhooks" class DiscordService < ChatNotificationService def title - "Discord Notifications" + s_("DiscordService|Discord Notifications") end def description - "Receive event notifications in Discord" + s_("DiscordService|Receive event notifications in Discord") end def self.to_param diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index fb73d430fb1..45de64a9990 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -7,11 +7,11 @@ class EmailsOnPushService < Service validates :recipients, presence: true, if: :valid_recipients? def title - 'Emails on push' + s_('EmailsOnPushService|Emails on push') end def description - 'Email the commits and diff of each push to a list of recipients.' + s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.') end def self.to_param @@ -45,11 +45,11 @@ class EmailsOnPushService < Service def fields domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ") [ - { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer", - help: "Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. #{domains})." }, - { type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs", - help: "Don't include possibly sensitive code diffs in notification body." }, - { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' } + { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"), + help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. %{domains}).") % { domains: domains } }, + { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"), + help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") }, + { type: 'textarea', name: 'recipients', placeholder: s_('EmailsOnPushService|Emails separated by whitespace') } ] end end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index d2835c6ac82..593ce69b0fd 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -6,11 +6,11 @@ class ExternalWikiService < Service validates :external_wiki_url, presence: true, public_url: true, if: :activated? def title - 'External Wiki' + s_('ExternalWikiService|External Wiki') end def description - 'Replaces the link to the internal wiki with a link to an external wiki.' + s_('ExternalWikiService|Replaces the link to the internal wiki with a link to an external wiki.') end def self.to_param @@ -19,7 +19,7 @@ class ExternalWikiService < Service def fields [ - { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki', required: true } + { type: 'text', name: 'external_wiki_url', placeholder: s_('ExternalWikiService|The URL of the external Wiki'), required: true } ] end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 76624263aab..094488cb431 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -9,7 +9,7 @@ class FlowdockService < Service end def description - 'Flowdock is a collaboration web app for technical teams.' + s_('FlowdockService|Flowdock is a collaboration web app for technical teams.') end def self.to_param @@ -18,7 +18,7 @@ class FlowdockService < Service def fields [ - { type: 'text', name: 'token', placeholder: 'Flowdock Git source token', required: true } + { type: 'text', name: 'token', placeholder: s_('FlowdockService|Flowdock Git source token'), required: true } ] end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb new file mode 100644 index 00000000000..a69b7b4c4b6 --- /dev/null +++ b/app/models/project_services/hipchat_service.rb @@ -0,0 +1,311 @@ +# frozen_string_literal: true + +class HipchatService < Service + include ActionView::Helpers::SanitizeHelper + + MAX_COMMITS = 3 + HIPCHAT_ALLOWED_TAGS = %w[ + a b i strong em br img pre code + table th tr td caption colgroup col thead tbody tfoot + ul ol li dl dt dd + ].freeze + + prop_accessor :token, :room, :server, :color, :api_version + boolean_accessor :notify_only_broken_pipelines, :notify + validates :token, presence: true, if: :activated? + + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_pipelines = true + end + end + + def title + 'HipChat' + end + + def description + 'Private group chat and IM' + end + + def self.to_param + 'hipchat' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: 'Room token', required: true }, + { type: 'text', name: 'room', placeholder: 'Room name or ID' }, + { type: 'checkbox', name: 'notify' }, + { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) }, + { type: 'text', name: 'api_version', + placeholder: 'Leave blank for default (v2)' }, + { type: 'text', name: 'server', + placeholder: 'Leave blank for default. https://hipchat.example.com' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' } + ] + end + + def self.supported_events + %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + message = create_message(data) + return unless message.present? + + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend + end + + def test(data) + begin + result = execute(data) + rescue StandardError => error + return { success: false, result: error } + end + + { success: true, result: result } + end + + private + + def gate + options = { api_version: api_version.present? ? api_version : 'v2' } + options[:server_url] = server unless server.blank? + @gate ||= HipChat::Client.new(token, options) + end + + def message_options(data = nil) + { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } + end + + def create_message(data) + object_kind = data[:object_kind] + + case object_kind + when "push", "tag_push" + create_push_message(data) + when "issue" + create_issue_message(data) unless update?(data) + when "merge_request" + create_merge_request_message(data) unless update?(data) + when "note" + create_note_message(data) + when "pipeline" + create_pipeline_message(data) if should_pipeline_be_notified?(data) + end + end + + def render_line(text) + markdown(text.lines.first.chomp, pipeline: :single_line) if text + end + + def create_push_message(push) + ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' + ref = Gitlab::Git.ref_name(push[:ref]) + + before = push[:before] + after = push[:after] + + message = [] + message << "#{push[:user_name]} " + + if Gitlab::Git.blank_ref?(before) + message << "pushed new #{ref_type} <a href=\""\ + "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\ + " to #{project_link}\n" + elsif Gitlab::Git.blank_ref?(after) + message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n" + else + message << "pushed to #{ref_type} <a href=\""\ + "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> " + message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> " + message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" + + push[:commits].take(MAX_COMMITS).each do |commit| + message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" + end + + if push[:commits].count > MAX_COMMITS + message << "<br />... #{push[:commits].count - MAX_COMMITS} more commits" + end + end + + message.join + end + + def markdown(text, options = {}) + return "" unless text + + context = { + project: project, + pipeline: :email + } + + Banzai.render(text, context) + + context.merge!(options) + + html = Banzai.render_and_post_process(text, context) + sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) + + sanitized_html.truncate(200, separator: ' ', omission: '...') + end + + def create_issue_message(data) + user_name = data[:user][:name] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + title = render_line(obj_attr[:title]) + state = obj_attr[:state] + issue_iid = obj_attr[:iid] + issue_url = obj_attr[:url] + description = obj_attr[:description] + + issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" + + message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"] + message << "<pre>#{markdown(description)}</pre>" + + message.join + end + + def create_merge_request_message(data) + user_name = data[:user][:name] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + merge_request_id = obj_attr[:iid] + state = obj_attr[:state] + description = obj_attr[:description] + title = render_line(obj_attr[:title]) + + merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" + merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" + message = ["#{user_name} #{state} #{merge_request_link} in " \ + "#{project_link}: <b>#{title}</b>"] + + message << "<pre>#{markdown(description)}</pre>" + message.join + end + + def format_title(title) + "<b>#{render_line(title)}</b>" + end + + def create_note_message(data) + data = HashWithIndifferentAccess.new(data) + user_name = data[:user][:name] + + obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) + note = obj_attr[:note] + note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + commit_id = nil + + case noteable_type + when "Commit" + commit_attr = HashWithIndifferentAccess.new(data[:commit]) + commit_id = commit_attr[:id] + subject_desc = commit_id + subject_desc = Commit.truncate_sha(subject_desc) + subject_type = "commit" + title = format_title(commit_attr[:message]) + when "Issue" + subj_attr = HashWithIndifferentAccess.new(data[:issue]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "issue" + title = format_title(subj_attr[:title]) + when "MergeRequest" + subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) + subject_id = subj_attr[:iid] + subject_desc = "!#{subject_id}" + subject_type = "merge request" + title = format_title(subj_attr[:title]) + when "Snippet" + subj_attr = HashWithIndifferentAccess.new(data[:snippet]) + subject_id = subj_attr[:id] + subject_desc = "##{subject_id}" + subject_type = "snippet" + title = format_title(subj_attr[:title]) + end + + subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>" + message = ["#{user_name} commented on #{subject_html} in #{project_link}: "] + message << title + + message << "<pre>#{markdown(note, ref: commit_id)}</pre>" + message.join + end + + def create_pipeline_message(data) + pipeline_attributes = data[:object_attributes] + pipeline_id = pipeline_attributes[:id] + ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + ref = pipeline_attributes[:ref] + user_name = (data[:user] && data[:user][:name]) || 'API' + status = pipeline_attributes[:status] + duration = pipeline_attributes[:duration] + + branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>" + pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>" + + "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" + end + + def message_color(data) + pipeline_status_color(data) || color || 'yellow' + end + + def pipeline_status_color(data) + return unless data && data[:object_kind] == 'pipeline' + + case data[:object_attributes][:status] + when 'success' + 'green' + else + 'red' + end + end + + def project_name + project.full_name.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def project_link + "<a href=\"#{project_url}\">#{project_name}</a>" + end + + def update?(data) + data[:object_attributes][:action] == 'update' + end + + def humanized_status(status) + case status + when 'success' + 'passed' + else + status + end + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end +end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 81302c516c2..ebf28dc842c 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -11,7 +11,7 @@ class JiraService < IssueTrackerService validates :password, presence: true, if: :activated? validates :jira_issue_transition_id, - format: { with: Gitlab::Regex.jira_transition_id_regex, message: "transition ids can have only numbers which can be split with , or ;" }, + format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") }, allow_blank: true # JIRA cloud version is deprecating authentication via username and password. @@ -86,7 +86,7 @@ class JiraService < IssueTrackerService if self.properties && self.properties['description'].present? self.properties['description'] else - 'Jira issue tracker' + s_('JiraService|Jira issue tracker') end end @@ -96,11 +96,11 @@ class JiraService < IssueTrackerService def fields [ - { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true }, - { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' }, - { type: 'text', name: 'username', title: 'Username or Email', placeholder: 'Use a username for server version and an email for cloud version', required: true }, - { type: 'password', name: 'password', title: 'Password or API token', placeholder: 'Use a password for server version and an API token for cloud version', required: true }, - { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID(s)', placeholder: 'Use , or ; to separate multiple transition IDs' } + { type: 'text', name: 'url', title: s_('JiraService|Web URL'), placeholder: 'https://jira.example.com', required: true }, + { type: 'text', name: 'api_url', title: s_('JiraService|JIRA API URL'), placeholder: s_('JiraService|If different from Web URL') }, + { type: 'text', name: 'username', title: s_('JiraService|Username or Email'), placeholder: s_('JiraService|Use a username for server version and an email for cloud version'), required: true }, + { type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true }, + { type: 'text', name: 'jira_issue_transition_id', title: s_('JiraService|Transition ID(s)'), placeholder: s_('JiraService|Use , or ; to separate multiple transition IDs') } ] end @@ -139,7 +139,7 @@ class JiraService < IssueTrackerService def create_cross_reference_note(mentioned, noteable, author) unless can_cross_reference?(noteable) - return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled." + return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) } end jira_issue = jira_request { client.Issue.find(mentioned.id) } @@ -338,9 +338,9 @@ class JiraService < IssueTrackerService def self.event_description(event) case event when "merge_request", "merge_request_events" - "JIRA comments will be created when an issue gets referenced in a merge request." + s_("JiraService|JIRA comments will be created when an issue gets referenced in a merge request.") when "commit", "commit_events" - "JIRA comments will be created when an issue gets referenced in a commit." + s_("JiraService|JIRA comments will be created when an issue gets referenced in a commit.") end end end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index d60a6a7efa3..7ba69370f14 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -10,11 +10,11 @@ class PipelinesEmailService < Service end def title - 'Pipelines emails' + _('Pipelines emails') end def description - 'Email the pipelines status to a list of recipients.' + _('Email the pipelines status to a list of recipients.') end def self.to_param @@ -51,7 +51,7 @@ class PipelinesEmailService < Service [ { type: 'textarea', name: 'recipients', - placeholder: 'Emails separated by comma', + placeholder: _('Emails separated by comma'), required: true }, { type: 'checkbox', name: 'notify_only_broken_pipelines' } diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 617e502b639..c15993bdc06 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -11,7 +11,7 @@ class PivotaltrackerService < Service end def description - 'Project Management Software (Source Commits Endpoint)' + s_('PivotalTrackerService|Project Management Software (Source Commits Endpoint)') end def self.to_param @@ -23,14 +23,14 @@ class PivotaltrackerService < Service { type: 'text', name: 'token', - placeholder: 'Pivotal Tracker API token.', + placeholder: s_('PivotalTrackerService|Pivotal Tracker API token.'), required: true }, { type: 'text', name: 'restrict_to_branch', - placeholder: 'Comma-separated list of branches which will be ' \ - 'automatically inspected. Leave blank to include all branches.' + placeholder: s_('PivotalTrackerService|Comma-separated list of branches which will be ' \ + 'automatically inspected. Leave blank to include all branches.') } ] end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 4e48c348b45..0d35bab7f80 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -11,7 +11,7 @@ class PushoverService < Service end def description - 'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.' + s_('PushoverService|Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.') end def self.to_param @@ -20,15 +20,15 @@ class PushoverService < Service def fields [ - { type: 'text', name: 'api_key', placeholder: 'Your application key', required: true }, - { type: 'text', name: 'user_key', placeholder: 'Your user key', required: true }, - { type: 'text', name: 'device', placeholder: 'Leave blank for all active devices' }, + { type: 'text', name: 'api_key', placeholder: s_('PushoverService|Your application key'), required: true }, + { type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true }, + { type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') }, { type: 'select', name: 'priority', required: true, choices: [ - ['Lowest Priority', -2], - ['Low Priority', -1], - ['Normal Priority', 0], - ['High Priority', 1] + [s_('PushoverService|Lowest Priority'), -2], + [s_('PushoverService|Low Priority'), -1], + [s_('PushoverService|Normal Priority'), 0], + [s_('PushoverService|High Priority'), 1] ], default_choice: 0 }, { type: 'select', name: 'sound', choices: @@ -73,15 +73,15 @@ class PushoverService < Service message = if Gitlab::Git.blank_ref?(before) - "#{data[:user_name]} pushed new branch \"#{ref}\"." + s_("PushoverService|%{user_name} pushed new branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref } elsif Gitlab::Git.blank_ref?(after) - "#{data[:user_name]} deleted branch \"#{ref}\"." + s_("PushoverService|%{user_name} deleted branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref } else - "#{data[:user_name]} push to branch \"#{ref}\"." + s_("PushoverService|%{user_name} push to branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref } end if data[:total_commits_count] > 0 - message = [message, "Total commits count: #{data[:total_commits_count]}"].join("\n") + message = [message, s_("PushoverService|Total commits count: %{total_commits_count}") % { total_commits_count: data[:total_commits_count] }].join("\n") end pushover_data = { @@ -92,7 +92,7 @@ class PushoverService < Service title: "#{project.full_name}", message: message, url: data[:project][:web_url], - url_title: "See project #{project.full_name}" + url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name } } # Sound parameter MUST NOT be sent to API if not selected diff --git a/app/models/service.rb b/app/models/service.rb index c6d5eb353dc..de549becf71 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -255,6 +255,7 @@ class Service < ApplicationRecord external_wiki flowdock hangouts_chat + hipchat irker jira kubernetes diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb index b4645462314..81415eb383b 100644 --- a/app/models/u2f_registration.rb +++ b/app/models/u2f_registration.rb @@ -19,7 +19,7 @@ class U2fRegistration < ApplicationRecord user: user, name: params[:name]) rescue JSON::ParserError, NoMethodError, ArgumentError - registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.') + registration.errors.add(:base, _('Your U2F device did not send a valid JSON response.')) rescue U2F::Error => e registration.errors.add(:base, e.message) end diff --git a/app/models/upload.rb b/app/models/upload.rb index 9bffdcdb2e7..ca74f16b3b8 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -45,7 +45,7 @@ class Upload < ApplicationRecord end def absolute_path - raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local? + raise ObjectStorage::RemoteStoreError, _("Remote object has no absolute path.") unless local? return path unless relative_path? uploader_class.absolute_path(self) @@ -71,10 +71,10 @@ class Upload < ApplicationRecord # Help sysadmins find missing upload files if persisted? && !exist if Gitlab::Sentry.enabled? - Raven.capture_message("Upload file does not exist", extra: self.attributes) + Raven.capture_message(_("Upload file does not exist"), extra: self.attributes) end - Gitlab::Metrics.counter(:upload_file_does_not_exist_total, 'The number of times an upload record could not find its file').increment + Gitlab::Metrics.counter(:upload_file_does_not_exist_total, _('The number of times an upload record could not find its file')).increment end exist diff --git a/app/models/user.rb b/app/models/user.rb index d3524bfd6ae..551eb58a4de 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -517,7 +517,7 @@ class User < ApplicationRecord def ghost email = 'ghost%s@example.com' unique_internal(where(ghost: true), 'ghost', email) do |u| - u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' + u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.') u.name = 'Ghost User' end end @@ -537,20 +537,16 @@ class User < ApplicationRecord username end - def self.internal_attributes - [:ghost] - end - def internal? - self.class.internal_attributes.any? { |a| self[a] } + ghost? end def self.internal - where(Hash[internal_attributes.zip([true] * internal_attributes.size)]) + where(ghost: true) end def self.non_internal - where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND ")) + where('ghost IS NOT TRUE') end # @@ -626,32 +622,32 @@ class User < ApplicationRecord def namespace_move_dir_allowed if namespace&.any_project_has_container_registry_tags? - errors.add(:username, 'cannot be changed if a personal project has container registry tags.') + errors.add(:username, _('cannot be changed if a personal project has container registry tags.')) end end def unique_email if !emails.exists?(email: email) && Email.exists?(email: email) - errors.add(:email, 'has already been taken') + errors.add(:email, _('has already been taken')) end end def owns_notification_email return if temp_oauth_email? - errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email) + errors.add(:notification_email, _("is not an email you own")) unless all_emails.include?(notification_email) end def owns_public_email return if public_email.blank? - errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) + errors.add(:public_email, _("is not an email you own")) unless all_emails.include?(public_email) end def owns_commit_email return if read_attribute(:commit_email).blank? - errors.add(:commit_email, "is not an email you own") unless verified_emails.include?(commit_email) + errors.add(:commit_email, _("is not an email you own")) unless verified_emails.include?(commit_email) end # Define commit_email-related attribute methods explicitly instead of relying diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 909da4316d0..cd4c7895587 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -31,7 +31,9 @@ class WikiPage pages.each_with_object([]) do |page, grouped_pages| next grouped_pages << page unless page.directory.present? - directory = grouped_pages.find { |dir| dir.slug == page.directory } + directory = grouped_pages.find do |obj| + obj.is_a?(WikiDirectory) && obj.slug == page.directory + end next directory.pages << page if directory diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 1f62b3eb4de..b37ac510ff4 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -7,6 +7,13 @@ module Clusters return unless app.scheduled? app.make_installing! + + install + end + + private + + def install log_event(:begin_install) helm_api.install(install_command) @@ -18,7 +25,7 @@ module Clusters app.make_errored!("Kubernetes error: #{e.error_code}") rescue StandardError => e log_error(e) - app.make_errored!("Can't start installation process.") + app.make_errored!('Failed to install.') end end end diff --git a/app/services/clusters/applications/patch_service.rb b/app/services/clusters/applications/patch_service.rb index c3d317e226b..977a5e91041 100644 --- a/app/services/clusters/applications/patch_service.rb +++ b/app/services/clusters/applications/patch_service.rb @@ -8,6 +8,12 @@ module Clusters app.make_updating! + patch + end + + private + + def patch log_event(:begin_patch) helm_api.update(update_command) @@ -16,10 +22,10 @@ module Clusters ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => e log_error(e) - app.make_update_errored!("Kubernetes error: #{e.error_code}") + app.make_errored!("Kubernetes error: #{e.error_code}") rescue StandardError => e log_error(e) - app.make_update_errored!("Can't start update process.") + app.make_errored!('Failed to update.') end end end diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb index c34391bc8ad..813a9c4d071 100644 --- a/app/services/clusters/applications/upgrade_service.rb +++ b/app/services/clusters/applications/upgrade_service.rb @@ -6,24 +6,28 @@ module Clusters def execute return unless app.scheduled? - begin - app.make_updating! + app.make_updating! - log_event(:begin_upgrade) - # install_command works with upgrades too - # as it basically does `helm upgrade --install` - helm_api.update(install_command) + upgrade + end + + private + + def upgrade + # install_command works with upgrades too + # as it basically does `helm upgrade --install` + log_event(:begin_upgrade) + helm_api.update(install_command) - log_event(:schedule_wait_for_upgrade) - ClusterWaitForAppInstallationWorker.perform_in( - ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) - rescue Kubeclient::HttpError => e - log_error(e) - app.make_update_errored!("Kubernetes error: #{e.error_code}") - rescue StandardError => e - log_error(e) - app.make_update_errored!("Can't start upgrade process.") - end + log_event(:schedule_wait_for_upgrade) + ClusterWaitForAppInstallationWorker.perform_in( + ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_errored!('Failed to upgrade.') end end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 74aad3b1c94..8f1f25a7307 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -62,12 +62,16 @@ module Groups end def can_use_visibility_level? - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + unless Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level) deny_visibility_level(@group) return false end true end + + def visibility_level + params[:visibility].present? ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level] + end end end diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb index e50840a9158..33444c2a7dc 100644 --- a/app/services/users/activity_service.rb +++ b/app/services/users/activity_service.rb @@ -30,7 +30,7 @@ module Users return if @user.last_activity_on == today - lease = Gitlab::ExclusiveLease.new("acitvity_service:#{@user.id}", + lease = Gitlab::ExclusiveLease.new("activity_service:#{@user.id}", timeout: LEASE_TIMEOUT) return unless lease.try_obtain diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 6b8dd156874..5a47040874f 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -4,6 +4,6 @@ -# Text diff discussions - expanded = local_assigns.fetch(:expanded, true) %tr.notes_holder{ class: ('hide' unless expanded) } - %td.notes_content{ colspan: 3 } + %td.notes-content{ colspan: 3 } .content{ class: ('hide' unless expanded) } = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true } diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index 2e621c4082d..03b428714b9 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -1,17 +1,17 @@ - expanded = [*discussions_left, *discussions_right].any?(&:expanded?) %tr.notes_holder{ class: ('hide' unless expanded) } - if discussions_left - %td.notes_content.parallel.old{ colspan: 2 } + %td.notes-content.parallel.old{ colspan: 2 } .content{ class: ('hide' unless discussions_left.any?(&:expanded?)) } = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true } - else - %td.notes_content.parallel.old{ colspan: 2 } + %td.notes-content.parallel.old{ colspan: 2 } .content - if discussions_right - %td.notes_content.parallel.new{ colspan: 2 } + %td.notes-content.parallel.new{ colspan: 2 } .content{ class: ('hide' unless discussions_right.any?(&:expanded?)) } = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true } - else - %td.notes_content.parallel.new{ colspan: 2 } + %td.notes-content.parallel.new{ colspan: 2 } .content diff --git a/app/views/projects/_classification_policy_settings.html.haml b/app/views/projects/_classification_policy_settings.html.haml index 57c7a718d53..5a766ab024f 100644 --- a/app/views/projects/_classification_policy_settings.html.haml +++ b/app/views/projects/_classification_policy_settings.html.haml @@ -1,8 +1,6 @@ - if ::Gitlab::ExternalAuthorization.enabled? - .form-group - = f.label :external_authorization_classification_label, class: 'label-bold' do - = s_('ExternalAuthorizationService|Classification Label') - %span.light (optional) + .form-group.col-md-9 + = f.label :external_authorization_classification_label, _('Classification Label (optional)'), class: 'label-bold' = f.text_field :external_authorization_classification_label, class: "form-control" %span.form-text.text-muted = external_classification_label_help_message diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index 409b6dba9ca..1056977886a 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -1,42 +1,33 @@ - return unless Gitlab::CurrentSettings.project_export_enabled? - project = local_assigns.fetch(:project) -- expanded = Rails.env.test? -%section.settings.no-animate#js-export-project{ class: ('expanded' if expanded) } - .settings-header - %h4 - Export project - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' - %p - Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. - .settings-content - .bs-callout.bs-callout-info - %p.append-bottom-0 - %p - The following items will be exported: - %ul - %li Project and wiki repositories - %li Project uploads - %li Project configuration, including services - %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities - %li LFS objects - %p - The following items will NOT be exported: - %ul - %li Job traces and artifacts - %li Container registry images - %li CI variables - %li Webhooks - %li Any encrypted tokens - %p - Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page. - - if project.export_status == :finished - = link_to 'Download export', download_export_project_path(project), - rel: 'nofollow', download: '', method: :get, class: "btn btn-default" - = link_to 'Generate new export', generate_new_export_project_path(project), - method: :post, class: "btn btn-default" - - else - = link_to 'Export project', export_project_path(project), - method: :post, class: "btn btn-default" +.sub-section + %h4= _('Export project') + %p= _('Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.') + + .bs-callout.bs-callout-info + %p.append-bottom-0 + %p= _('The following items will be exported:') + %ul + %li= _('Project and wiki repositories') + %li= _('Project uploads') + %li= _('Project configuration, including services') + %li= _('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities') + %li= _('LFS objects') + %p= _('The following items will NOT be exported:') + %ul + %li= _('Job traces and artifacts') + %li= _('Container registry images') + %li= _('CI variables') + %li= _('Webhooks') + %li= _('Any encrypted tokens') + %p= _('Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.') + - if project.export_status == :finished + = link_to _('Download export'), download_export_project_path(project), + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" + = link_to _('Generate new export'), generate_new_export_project_path(project), + method: :post, class: "btn btn-default" + - else + = link_to _('Export project'), export_project_path(project), + method: :post, class: "btn btn-default" diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index e45c5be76de..c84c376d57b 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -10,5 +10,5 @@ - actions.each do |action| - next unless can?(current_user, :update_build, action) %li - = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do + = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do %span= action.name diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 56427a74d56..018c5b38536 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -1,7 +1,7 @@ - too_big = diff_file.diff_lines.count > Commit::DIFF_SAFE_LINES - if too_big .suppressed-container - %a.show-suppressed-diff.js-show-suppressed-diff= _("Changes suppressed. Click to show.") + %a.show-suppressed-diff.cursor-pointer.js-show-suppressed-diff= _("Changes suppressed. Click to show.") %table.text-file.diff-wrap-lines.code.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' } = render partial: "projects/diffs/line", diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index abf2fb7dc57..1a3e4a5d608 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -3,210 +3,155 @@ - @content_class = "limit-container-width" unless fluid_layout - expanded = Rails.env.test? -.project-edit-container - %section.settings.general-settings.no-animate#js-general-project-settings{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, tags, avatar') - %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') - %p= _('Update your project name, tags, description and avatar.') - - .settings-content - .project-edit-errors - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| - %input{ name: 'update_section', type: 'hidden', value: 'js-general-project-settings' } - %fieldset - .row - .form-group.col-md-9 - = f.label :name, class: 'label-bold', for: 'project_name_edit' do - Project name - = f.text_field :name, class: "form-control", id: "project_name_edit" - - .form-group.col-md-3 - = f.label :id, class: 'label-bold' do - Project ID - = f.text_field :id, class: 'form-control', readonly: true - - .form-group - = f.label :description, class: 'label-bold' do - Project description - %span.light (optional) - = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - - = render 'projects/classification_policy_settings', f: f - - = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project - - .form-group - = f.label :tag_list, "Topics", class: 'label-bold' - = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" - %p.form-text.text-muted Separate topics with commas. - - .form-group.prepend-top-default.append-bottom-20 - .avatar-container.s90 - = project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90') - = f.label :avatar, _('Project avatar'), class: 'label-bold d-block' - = render 'shared/choose_avatar_button', f: f - - if @project.avatar? - %hr - = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' - - = f.submit 'Save changes', class: "btn btn-success js-btn-success-general-project-settings" - - %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions') - %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') - %p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.') - - .settings-content - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| - %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' } - -# haml-lint:disable InlineJavaScript - %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project) - .js-project-permissions-form - = f.submit 'Save changes', class: "btn btn-success" - - = render_if_exists 'projects/issues_settings' - - %section.qa-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests') - %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') - %p= _('Choose your merge method, set up a default merge request description template.') - - .settings-content - = render_if_exists 'shared/promotions/promote_mr_features' - - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| - %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } - = render 'projects/merge_request_settings', form: f - = f.submit 'Save changes', class: "btn btn-success qa-save-merge-request-changes" - - = render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded - - - %section.settings.no-animate{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = s_('ProjectSettings|Badges') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' - %p - = s_('ProjectSettings|Customize your project badges.') - = link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges') - .settings-content - = render 'shared/badges/badge_settings' - - = render_if_exists 'projects/service_desk_settings' - = render 'export', project: @project - - %section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced') - %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') - %p= _('Housekeeping, export, path, transfer, remove, archive.') - - .settings-content +%section.settings.general-settings.no-animate.expanded#js-general-settings + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar') + %button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse') + %p= _('Update your project name, topics, description and avatar.') + .settings-content= render 'projects/settings/general' + +%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions') + %button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') + %p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.') + + .settings-content + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| + %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' } + %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project) + .js-project-permissions-form + = f.submit _('Save changes'), class: "btn btn-success" + +%section.qa-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests') + %button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') + %p= _('Choose your merge method, set up a default merge request description template.') + + .settings-content + = render_if_exists 'shared/promotions/promote_mr_features' + + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f| + %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } + = render 'projects/merge_request_settings', form: f + = f.submit _('Save changes'), class: "btn btn-success qa-save-merge-request-changes" + += render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded + + +%section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = s_('ProjectSettings|Badges') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = s_('ProjectSettings|Customize your project badges.') + = link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges') + .settings-content + = render 'shared/badges/badge_settings' + += render_if_exists 'projects/settings/default_issue_template' + += render_if_exists 'projects/service_desk_settings' + +%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced') + %button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') + %p= _('Housekeeping, export, path, transfer, remove, archive.') + + .settings-content + .sub-section + %h4= _('Housekeeping') + %p= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.') + = link_to _('Run housekeeping'), housekeeping_project_path(@project), + method: :post, class: "btn btn-default" + + = render 'export', project: @project + + - if can? current_user, :archive_project, @project .sub-section - %h4 Housekeeping - %p - Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects. - = link_to 'Run housekeeping', housekeeping_project_path(@project), - method: :post, class: "btn btn-default" - - if can? current_user, :archive_project, @project - .sub-section - %h4.warning-title - - if @project.archived? - Unarchive project - - else - Archive project + %h4.warning-title - if @project.archived? - %p - Unarchiving the project will restore people's ability to make changes to it. - The repository can be committed to, and issues, comments and other entities can be created. - %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive project', unarchive_project_path(@project), - data: { confirm: "Are you sure that you want to unarchive this project?" }, - method: :post, class: "btn btn-success" + = _('Unarchive project') - else - %p - Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. - %strong The repository cannot be committed to, and no issues, comments or other entities can be created. - = link_to 'Archive project', archive_project_path(@project), - data: { confirm: "Are you sure that you want to archive this project?" }, - method: :post, class: "btn btn-warning" - .sub-section.rename-repository - %h4.warning-title - Rename repository - = render 'projects/errors' - = form_for([@project.namespace.becomes(Namespace), @project]) do |f| - .form-group.project_name_holder - = f.label :name, class: 'label-bold' do - Project name - .form-group - = f.text_field :name, class: "form-control" + = _('Archive project') + - if @project.archived? + %p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>").html_safe + = link_to _('Unarchive project'), unarchive_project_path(@project), + data: { confirm: _("Are you sure that you want to unarchive this project?") }, + method: :post, class: "btn btn-success" + - else + %p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>").html_safe + = link_to _('Archive project'), archive_project_path(@project), + data: { confirm: _("Are you sure that you want to archive this project?") }, + method: :post, class: "btn btn-warning" + .sub-section.rename-repository + %h4.warning-title= _('Change path') + = render 'projects/errors' + = form_for([@project.namespace.becomes(Namespace), @project]) do |f| + .form-group + = f.label :path, _('Path'), class: 'label-bold' + .form-group + .input-group + .input-group-prepend + .input-group-text + #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/ + = f.text_field :path, class: 'form-control qa-project-path-field h-auto' + %ul + %li= _("Be careful. Renaming a project's repository can have unintended side effects.") + %li= _('You will need to update your local repositories to point to the new location.') + - if @project.deployment_platform.present? + %li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.') + = f.submit _('Change path'), class: "btn btn-warning qa-change-path-button" + + - if can?(current_user, :change_namespace, @project) + .sub-section + %h4.danger-title= _('Transfer project') + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| .form-group - = f.label :path, class: 'label-bold' do - %span Path + = label_tag :new_namespace_id, nil, class: 'label-bold' do + %span= _('Select a new namespace') .form-group - .input-group - .input-group-prepend - .input-group-text - #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/ - = f.text_field :path, class: 'form-control' + = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' %ul - %li Be careful. Renaming a project's repository can have unintended side effects. - %li You will need to update your local repositories to point to the new location. - - if @project.deployment_platform.present? - %li Your deployment services will be broken, you will need to manually fix the services after renaming. - = f.submit 'Rename project', class: "btn btn-warning" - - if can?(current_user, :change_namespace, @project) - .sub-section - %h4.danger-title - Transfer project - = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| - .form-group - = label_tag :new_namespace_id, nil, class: 'label-bold' do - %span Select a new namespace - .form-group - = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' - %ul - %li Be careful. Changing the project's namespace can have unintended side effects. - %li You can only transfer the project to namespaces you manage. - %li You will need to update your local repositories to point to the new location. - %li Project visibility level will be changed to match namespace rules when transferring to a group. - = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - - if @project.forked? && can?(current_user, :remove_fork_project, @project) - .sub-section - %h4.danger-title - Remove fork relationship + %li= _("Be careful. Changing the project's namespace can have unintended side effects.") + %li= _('You can only transfer the project to namespaces you manage.') + %li= _('You will need to update your local repositories to point to the new location.') + %li= _('Project visibility level will be changed to match namespace rules when transferring to a group.') + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + .sub-section + %h4.danger-title= _('Remove fork relationship') + %p + = _('This will remove the fork relationship to source project') + = succeed "." do + - if @project.fork_source + = link_to(fork_source_name(@project), project_path(@project.fork_source)) + - else + = fork_source_name(@project) + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| %p - This will remove the fork relationship to source project - = succeed "." do - - if @project.fork_source - = link_to(fork_source_name(@project), project_path(@project.fork_source)) - - else - = fork_source_name(@project) - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| - %p - %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - - if can?(current_user, :remove_project, @project) - .sub-section - %h4.danger-title - Remove project + %strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.') + = button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + + - if can?(current_user, :remove_project, @project) + .sub-section + %h4.danger-title= _('Remove project') + %p= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.') + = form_tag(project_path(@project), method: :delete) do %p - Removing the project will delete its repository and all related resources including issues, merge requests etc. - = form_tag(project_path(@project), method: :delete) do - %p - %strong Removed projects cannot be restored! - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + %strong= _('Removed projects cannot be restored!') + = button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } .save-project-loader.hide .center %h2 %i.fa.fa-spinner.fa-spin - Saving project. - %p Please wait a moment, this page will automatically refresh when ready. + = _('Saving project.') + %p= _('Please wait a moment, this page will automatically refresh when ready.') = render 'shared/confirm_modal', phrase: @project.path diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml index cbd5c54cecc..1fbe34cfff3 100644 --- a/app/views/projects/environments/_form.html.haml +++ b/app/views/projects/environments/_form.html.haml @@ -17,5 +17,5 @@ = f.url_field :external_url, class: 'form-control' .form-actions - = f.submit _('Save'), class: 'btn btn-save' + = f.submit _('Save'), class: 'btn btn-success' = link_to _('Cancel'), project_environments_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 5111c9fab8d..79c586eef73 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -82,7 +82,8 @@ help_page_path: suggest_changes_help_path, current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json, project_path: project_path(@merge_request.project), - changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg') } } + changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), + is_fluid_layout: fluid_layout.to_s } } .mr-loading-status = spinner diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 1121cf06b5c..396e5da87bc 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f| = form_errors(@schedule) .form-group.row .col-md-9 @@ -11,12 +11,12 @@ .form-group.row .col-md-9 = f.label :cron_timezone, _('Cron Timezone'), class: 'label-bold' - = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown w-100', dropdown_class: 'w-100', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group.row .col-md-9 = f.label :ref, _('Target Branch'), class: 'label-bold' - = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) + = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown w-100', dropdown_class: 'git-revision-dropdown w-100', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group.row.js-ci-variable-list-section .col-md-9 diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml new file mode 100644 index 00000000000..380430ff52b --- /dev/null +++ b/app/views/projects/settings/_general.html.haml @@ -0,0 +1,42 @@ += form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f| + %input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' } + = form_errors(@project) + + %fieldset + .row + .form-group.col-md-5 + = f.label :name, class: 'label-bold', for: 'project_name_edit' do + = _('Project name') + = f.text_field :name, class: 'form-control qa-project-name-field', id: "project_name_edit" + + .form-group.col-md-7 + = f.label :id, class: 'label-bold' do + = _('Project ID') + = f.text_field :id, class: 'form-control w-auto', readonly: true + + .row + .form-group.col-md-9 + = f.label :tag_list, _('Topics'), class: 'label-bold' + = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" + %p.form-text.text-muted= _('Separate topics with commas.') + + .row + .form-group.col-md-9 + = f.label :description, _('Project description (optional)'), class: 'label-bold' + = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250 + + .row= render_if_exists 'projects/classification_policy_settings', f: f + + .row= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project + + .form-group.prepend-top-default.append-bottom-20 + .avatar-container.s90 + = project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90') + = f.label :avatar, _('Project avatar'), class: 'label-bold d-block' + = render 'shared/choose_avatar_button', f: f + - if @project.avatar? + %hr + = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' + + + = f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button" diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 1dcf4369253..3967c8148d2 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -2,8 +2,7 @@ .modal-dialog .modal-content .modal-header - %h3.page-title - Confirmation required + %h3.page-title= _('Confirmation required') %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %span{ "aria-hidden": true } × @@ -11,8 +10,7 @@ %p.text-danger.js-confirm-text %p - This action can lead to data loss. - To prevent accidental actions we ask you to confirm your intention. + %span.js-warning-text= _('This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.') %br Please type %code.js-confirm-danger-match= phrase @@ -21,4 +19,4 @@ .form-group = text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input' .form-actions - = submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit" + = submit_tag _('Confirm'), class: "btn btn-danger js-confirm-danger-submit" diff --git a/changelogs/unreleased/59921-pipeline-schedule.yml b/changelogs/unreleased/59921-pipeline-schedule.yml new file mode 100644 index 00000000000..4227a047913 --- /dev/null +++ b/changelogs/unreleased/59921-pipeline-schedule.yml @@ -0,0 +1,5 @@ +--- +title: Replaces CSS with BS4 utility class for pipeline schedules +merge_request: +author: +type: other diff --git a/changelogs/unreleased/60224-btn-env.yml b/changelogs/unreleased/60224-btn-env.yml new file mode 100644 index 00000000000..5053ddb31fa --- /dev/null +++ b/changelogs/unreleased/60224-btn-env.yml @@ -0,0 +1,5 @@ +--- +title: Fixes actions dropdowns in environments page +merge_request: 27160 +author: +type: fixed diff --git a/changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml b/changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml new file mode 100644 index 00000000000..ce942777dca --- /dev/null +++ b/changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug where MR popover doesn't go away on mouse leave +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/60261-save-btn-env.yml b/changelogs/unreleased/60261-save-btn-env.yml new file mode 100644 index 00000000000..b0936198d2e --- /dev/null +++ b/changelogs/unreleased/60261-save-btn-env.yml @@ -0,0 +1,5 @@ +--- +title: Fixes create button background for Environments form +merge_request: 27161 +author: +type: fixed diff --git a/changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml b/changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml new file mode 100644 index 00000000000..ec5e9e4703b --- /dev/null +++ b/changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml @@ -0,0 +1,5 @@ +--- +title: Fix long file header names bug in diffs +merge_request: 27233 +author: +type: fixed diff --git a/changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml b/changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml deleted file mode 100644 index 731c9c10b00..00000000000 --- a/changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow to use untrusted Regexp via feature flag -merge_request: 26905 -author: -type: deprecated diff --git a/changelogs/unreleased/fix-api-group-visibility.yml b/changelogs/unreleased/fix-api-group-visibility.yml new file mode 100644 index 00000000000..7fbdcd729c6 --- /dev/null +++ b/changelogs/unreleased/fix-api-group-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Fix api group visibility +merge_request: 26896 +author: +type: fixed diff --git a/changelogs/unreleased/fix-pull-request-importer.yml b/changelogs/unreleased/fix-pull-request-importer.yml deleted file mode 100644 index 5f642a0710b..00000000000 --- a/changelogs/unreleased/fix-pull-request-importer.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve performance of PR import -merge_request: 27121 -author: -type: performance diff --git a/changelogs/unreleased/fj-bump-workhorse-version-8-6-0.yml b/changelogs/unreleased/fj-bump-workhorse-version-8-6-0.yml new file mode 100644 index 00000000000..e53499e21ba --- /dev/null +++ b/changelogs/unreleased/fj-bump-workhorse-version-8-6-0.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Workhorse to v8.6.0 +merge_request: 27260 +author: +type: fixed diff --git a/changelogs/unreleased/sh-disable-diff-instrumentation.yml b/changelogs/unreleased/sh-disable-diff-instrumentation.yml deleted file mode 100644 index 55f4c2a8510..00000000000 --- a/changelogs/unreleased/sh-disable-diff-instrumentation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Disable method instrumentation for diffs -merge_request: 27235 -author: -type: performance diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb new file mode 100644 index 00000000000..1879ecb15fb --- /dev/null +++ b/config/initializers/hipchat_client_patch.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb. +module HipChat + class Client + connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter + end + + class Room + connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter + end + + class User + connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter + end +end diff --git a/db/migrate/20190107151029_remove_hipchat_services.rb b/db/migrate/20190107151029_remove_hipchat_services.rb deleted file mode 100644 index 4741ec88907..00000000000 --- a/db/migrate/20190107151029_remove_hipchat_services.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class RemoveHipchatServices < ActiveRecord::Migration[5.0] - DOWNTIME = false - - def up - execute "DELETE FROM services WHERE type = 'HipchatService'" - end - - def down - # no-op - end -end diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index f406163aea0..72341a5c777 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -145,7 +145,6 @@ mountpoint └── gitlab-data ├── builds ├── git-data - ├── home-git ├── shared └── uploads ``` @@ -158,16 +157,11 @@ configuration to move each data location to a subdirectory: ```ruby git_data_dirs({"default" => { "path" => "/gitlab-nfs/gitlab-data/git-data"} }) -user['home'] = '/gitlab-nfs/gitlab-data/home' gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads' gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared' gitlab_ci['builds_directory'] = '/gitlab-nfs/gitlab-data/builds' ``` -To move the `git` home directory, all GitLab services must be stopped. Run -`gitlab-ctl stop && initctl stop gitlab-runsvdir`. Then continue with the -reconfigure. - Run `sudo gitlab-ctl reconfigure` to start using the central location. Please be aware that if you had existing data you will need to manually copy/rsync it to these new locations and then restart GitLab. @@ -197,14 +191,13 @@ are empty before attempting a restore. Read more about the ## Multiple NFS mounts -When using default Omnibus configuration you will need to share 5 data locations +When using default Omnibus configuration you will need to share 4 data locations between all GitLab cluster nodes. No other locations should be shared. The -following are the 5 locations need to be shared: +following are the 4 locations need to be shared: | Location | Description | Default configuration | | -------- | ----------- | --------------------- | | `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => { "path" => "/var/opt/gitlab/git-data"} })` -| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'` | `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'` | `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'` | `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'` diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 17d72b96a51..1373bd56fe3 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -174,6 +174,10 @@ behavior: 1. [Reconfigure GitLab][reconfigure]. +NOTE: **Note:** +`inplace_chroot` option might not work with the other features, such as [Pages Access Control](#access-control). +The [GitLab Pages README](https://gitlab.com/gitlab-org/gitlab-pages#caveats) has more information about caveats and workarounds. + ## Advanced configuration In addition to the wildcard domains, you can also have the option to configure diff --git a/doc/api/services.md b/doc/api/services.md index 1f84e2de7de..e8ae7ff78f4 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -449,6 +449,45 @@ Get Hangouts Chat service settings for a project. GET /projects/:id/services/hangouts-chat ``` +## HipChat + +Private group chat and IM + +### Create/Edit HipChat service + +Set HipChat service for a project. + +``` +PUT /projects/:id/services/hipchat +``` + +Parameters: + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `token` | string | true | Room token | +| `color` | string | false | The room color | +| `notify` | boolean | false | Enable notifications | +| `room` | string | false |Room name or ID | +| `api_version` | string | false | Leave blank for default (v2) | +| `server` | string | false | Leave blank for default. For example, `https://hipchat.example.com`. | + +### Delete HipChat service + +Delete HipChat service for a project. + +``` +DELETE /projects/:id/services/hipchat +``` + +### Get HipChat service settings + +Get HipChat service settings for a project. + +``` +GET /projects/:id/services/hipchat +``` + ## Irker (IRC gateway) Send IRC messages, on update, to a list of recipients through an Irker gateway. diff --git a/doc/integration/README.md b/doc/integration/README.md index f5bc0693b84..a539933f223 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -29,8 +29,8 @@ See the documentation below for details on how to configure these services. ## Project services -Integration with services such as Campfire, Flowdock, Pivotal Tracker, and Slack -are available in the form of a [Project Service][]. +Integration with services such as Campfire, Flowdock, HipChat, +Pivotal Tracker, and Slack are available in the form of a [Project Service][]. [Project Service]: ../user/project/integrations/project_services.md diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md new file mode 100644 index 00000000000..4ae9f6c6b2e --- /dev/null +++ b/doc/project_services/hipchat.md @@ -0,0 +1 @@ +This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md). diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 254e234a22c..0af2f8d2f54 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -41,7 +41,7 @@ Objects (usually binary and large) created by a build process. These can include ### Atlassian -A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Confluence, Bamboo. +A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo. ### Audit Log diff --git a/doc/user/index.md b/doc/user/index.md index 626246447f3..8164b31c37e 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -65,9 +65,7 @@ With GitLab Enterprise Edition, you can also: - View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html). - Leverage continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html). -You can also [integrate](project/integrations/project_services.md) GitLab with -numerous third-party applications, such as Mattermost, Microsoft Teams, Trello, -Slack, Bamboo CI, JIRA, and a lot more. +You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more. ## Projects diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md new file mode 100644 index 00000000000..0fd847d415f --- /dev/null +++ b/doc/user/project/integrations/hipchat.md @@ -0,0 +1,53 @@ +# Atlassian HipChat + +GitLab provides a way to send HipChat notifications upon a number of events, +such as when a user pushes code, creates a branch or tag, adds a comment, and +creates a merge request. + +## Setup + +GitLab requires the use of a HipChat v2 API token to work. v1 tokens are +not supported at this time. Note the differences between v1 and v2 tokens: + +HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 +token is allowed to send messages to *any* room. + +HipChat v2 API has tokens that are can be created using the Integrations tab +in the Group or Room admin page. By design, these are lightweight tokens that +allow GitLab to send messages only to *one* room. + +### Complete these steps in HipChat + +1. Go to: <https://admin.hipchat.com/admin> +1. Click on "Group Admin" -> "Integrations". +1. Find "Build Your Own!" and click "Create". +1. Select the desired room, name the integration "GitLab", and click "Create". +1. In the "Send messages to this room by posting this URL" column, you should +see a URL in the format: + +``` +https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token> +``` + +HipChat is now ready to accept messages from GitLab. Next, set up the HipChat +service in GitLab. + +### Complete these steps in GitLab + +1. Navigate to the project you want to configure for notifications. +1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) +1. Click "HipChat". +1. Select the "Active" checkbox. +1. Insert the `token` field from the URL into the `Token` field on the Web page. +1. Insert the `room` field from the URL into the `Room` field on the Web page. +1. Save or optionally click "Test Settings". + +## Troubleshooting + +If you do not see notifications, make sure you are using a HipChat v2 API +token, not a v1 token. + +Note that the v2 token is tied to a specific room. If you want to be able to +specify arbitrary rooms, you can create an API token for a specific user in +HipChat under "Account settings" and "API access". Use the `XXX` value under +`auth_token=XXX`. diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index e2f23827360..42c7824a125 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -36,6 +36,7 @@ Click on the service links to see further configuration instructions and details | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | | [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat | +| [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index 8582c45798f..953be7f3798 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # frozen_string_literal: true module API @@ -386,6 +387,44 @@ module API }, chat_notification_events ].flatten, + 'hipchat' => [ + { + required: true, + name: :token, + type: String, + desc: 'The room token' + }, + { + required: false, + name: :room, + type: String, + desc: 'The room name or ID' + }, + { + required: false, + name: :color, + type: String, + desc: 'The room color' + }, + { + required: false, + name: :notify, + type: Boolean, + desc: 'Enable notifications' + }, + { + required: false, + name: :api_version, + type: String, + desc: 'Leave blank for default (v2)' + }, + { + required: false, + name: :server, + type: String, + desc: 'Leave blank for default. https://hipchat.example.com' + } + ], 'irker' => [ { required: true, @@ -690,6 +729,7 @@ module API ::ExternalWikiService, ::FlowdockService, ::HangoutsChatService, + ::HipchatService, ::IrkerService, ::JiraService, ::KubernetesService, diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index bb4e536cf57..e7504051808 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -20,6 +20,10 @@ module API def provider :github end + + def provider_unauthorized + error!("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.", 401) + end end desc 'Import a GitHub project' do diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index a3c7de87765..8f9d5cf1e63 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -75,8 +75,8 @@ module Gitlab user.admin? || allowed_level?(level.to_i) end + # Level should be a numeric value, e.g. `20` # Return true if the specified level is allowed for the current user. - # Level should be a numeric value, e.g. `20`. def allowed_level?(level) valid_level?(level) && non_restricted_level?(level) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d562e76da42..4f401acccf9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -132,6 +132,12 @@ msgstr "" msgid "%{label_for_message} unavailable" msgstr "" +msgid "%{level_name} is not allowed in a %{group_level_name} group." +msgstr "" + +msgid "%{level_name} is not allowed since the fork source project has lower visibility." +msgstr "" + msgid "%{level_name} visibility has been restricted by the administrator." msgstr "" @@ -203,6 +209,12 @@ msgstr "" msgid "%{user_name} profile page" msgstr "" +msgid "'%{level}' is not a valid visibility level" +msgstr "" + +msgid "'%{source}' is not a import source" +msgstr "" + msgid "(external source)" msgstr "" @@ -867,6 +879,9 @@ msgstr "" msgid "Any" msgstr "" +msgid "Any encrypted tokens" +msgstr "" + msgid "Appearance" msgstr "" @@ -912,15 +927,27 @@ msgstr "" msgid "Archive jobs" msgstr "" +msgid "Archive project" +msgstr "" + msgid "Archived project! Repository and other project resources are read-only" msgstr "" msgid "Archived projects" msgstr "" +msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>" +msgstr "" + msgid "Are you sure" msgstr "" +msgid "Are you sure that you want to archive this project?" +msgstr "" + +msgid "Are you sure that you want to unarchive this project?" +msgstr "" + msgid "Are you sure you want to delete this pipeline schedule?" msgstr "" @@ -963,6 +990,18 @@ msgstr "" msgid "Artifacts" msgstr "" +msgid "AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):" +msgstr "" + +msgid "AsanaService|Asana - Teamwork without email" +msgstr "" + +msgid "AsanaService|Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches." +msgstr "" + +msgid "AsanaService|User Personal Access Token. User must have access to task, all comments will be attributed to this user." +msgstr "" + msgid "Ask your group maintainer to set up a group Runner." msgstr "" @@ -1206,6 +1245,30 @@ msgstr "" msgid "Badges|e.g. %{exampleUrl}" msgstr "" +msgid "BambooService|A continuous integration and build server" +msgstr "" + +msgid "BambooService|A user with API access, if applicable" +msgstr "" + +msgid "BambooService|Atlassian Bamboo CI" +msgstr "" + +msgid "BambooService|Bamboo build plan key like KEY" +msgstr "" + +msgid "BambooService|Bamboo root URL like https://bamboo.example.com" +msgstr "" + +msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo." +msgstr "" + +msgid "Be careful. Changing the project's namespace can have unintended side effects." +msgstr "" + +msgid "Be careful. Renaming a project's repository can have unintended side effects." +msgstr "" + msgid "Begin with the selected commit" msgstr "" @@ -1413,6 +1476,9 @@ msgstr "" msgid "CI Lint" msgstr "" +msgid "CI variables" +msgstr "" + msgid "CI/CD" msgstr "" @@ -1491,6 +1557,9 @@ msgstr "" msgid "Cannot modify managed Kubernetes cluster" msgstr "" +msgid "Cannot refer to a group milestone by an internal id!" +msgstr "" + msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded." msgstr "" @@ -1503,6 +1572,9 @@ msgstr "" msgid "Certificate (PEM)" msgstr "" +msgid "Change path" +msgstr "" + msgid "Change permissions" msgstr "" @@ -1707,6 +1779,9 @@ msgstr "" msgid "CiVariable|Validation failed" msgstr "" +msgid "Classification Label (optional)" +msgstr "" + msgid "ClassificationLabelUnavailable|is unavailable: %{reason}" msgstr "" @@ -2408,6 +2483,12 @@ msgstr "" msgid "Configure the way a user creates a new account." msgstr "" +msgid "Confirm" +msgstr "" + +msgid "Confirmation required" +msgstr "" + msgid "Connect" msgstr "" @@ -2417,6 +2498,9 @@ msgstr "" msgid "Container Registry" msgstr "" +msgid "Container registry images" +msgstr "" + msgid "ContainerRegistry|Created" msgstr "" @@ -2561,6 +2645,9 @@ msgstr "" msgid "Could not authorize chat nickname. Try again!" msgstr "" +msgid "Could not change HEAD: branch '%{branch}' does not exist" +msgstr "" + msgid "Could not connect to FogBugz, check your URL" msgstr "" @@ -3070,6 +3157,12 @@ msgstr "" msgid "Discard draft" msgstr "" +msgid "DiscordService|Discord Notifications" +msgstr "" + +msgid "DiscordService|Receive event notifications in Discord" +msgstr "" + msgid "Discover projects, groups and snippets. Share your projects with others" msgstr "" @@ -3109,6 +3202,9 @@ msgstr "" msgid "Download asset" msgstr "" +msgid "Download export" +msgstr "" + msgid "Download tar" msgstr "" @@ -3193,6 +3289,9 @@ msgstr "" msgid "Email patch" msgstr "" +msgid "Email the pipelines status to a list of recipients." +msgstr "" + msgid "EmailError|It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." msgstr "" @@ -3220,6 +3319,30 @@ msgstr "" msgid "Emails" msgstr "" +msgid "Emails separated by comma" +msgstr "" + +msgid "EmailsOnPushService|Disable code diffs" +msgstr "" + +msgid "EmailsOnPushService|Don't include possibly sensitive code diffs in notification body." +msgstr "" + +msgid "EmailsOnPushService|Email the commits and diff of each push to a list of recipients." +msgstr "" + +msgid "EmailsOnPushService|Emails on push" +msgstr "" + +msgid "EmailsOnPushService|Emails separated by whitespace" +msgstr "" + +msgid "EmailsOnPushService|Send from committer" +msgstr "" + +msgid "EmailsOnPushService|Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. %{domains})." +msgstr "" + msgid "Embed" msgstr "" @@ -3679,6 +3802,12 @@ msgstr "" msgid "Explore public groups" msgstr "" +msgid "Export project" +msgstr "" + +msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page." +msgstr "" + msgid "External Classification Policy Authorization" msgstr "" @@ -3697,15 +3826,21 @@ msgstr "" msgid "External authorization request timeout" msgstr "" -msgid "ExternalAuthorizationService|Classification Label" -msgstr "" - msgid "ExternalAuthorizationService|Classification label" msgstr "" msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used." msgstr "" +msgid "ExternalWikiService|External Wiki" +msgstr "" + +msgid "ExternalWikiService|Replaces the link to the internal wiki with a link to an external wiki." +msgstr "" + +msgid "ExternalWikiService|The URL of the external Wiki" +msgstr "" + msgid "Facebook" msgstr "" @@ -3715,12 +3850,18 @@ msgstr "" msgid "Failed Jobs" msgstr "" +msgid "Failed create wiki" +msgstr "" + msgid "Failed to change the owner" msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to create repository via gitlab-shell" +msgstr "" + msgid "Failed to create resources" msgstr "" @@ -3876,6 +4017,12 @@ msgstr "" msgid "FirstPushedBy|pushed by" msgstr "" +msgid "FlowdockService|Flowdock Git source token" +msgstr "" + +msgid "FlowdockService|Flowdock is a collaboration web app for technical teams." +msgstr "" + msgid "FogBugz Email" msgstr "" @@ -3981,6 +4128,9 @@ msgstr "" msgid "Generate a default set of labels" msgstr "" +msgid "Generate new export" +msgstr "" + msgid "Geo" msgstr "" @@ -4346,6 +4496,9 @@ msgstr "" msgid "Hook was successfully updated." msgstr "" +msgid "Housekeeping" +msgstr "" + msgid "Housekeeping successfully started" msgstr "" @@ -4556,6 +4709,9 @@ msgstr "" msgid "Incompatible Project" msgstr "" +msgid "Incompatible options set!" +msgstr "" + msgid "Indicates whether this runner can pick jobs without tags" msgstr "" @@ -4631,6 +4787,9 @@ msgstr "" msgid "Invalid Login or password" msgstr "" +msgid "Invalid feature" +msgstr "" + msgid "Invalid file." msgstr "" @@ -4640,6 +4799,9 @@ msgstr "" msgid "Invalid pin code" msgstr "" +msgid "Invalid repository path" +msgstr "" + msgid "Invalid two-factor code." msgstr "" @@ -4682,6 +4844,9 @@ msgstr "" msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable." msgstr "" +msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" +msgstr "" + msgid "Issues, merge requests, pushes, and comments." msgstr "" @@ -4697,6 +4862,48 @@ msgstr "" msgid "January" msgstr "" +msgid "JiraService|Events for %{noteable_model_name} are disabled." +msgstr "" + +msgid "JiraService|If different from Web URL" +msgstr "" + +msgid "JiraService|JIRA API URL" +msgstr "" + +msgid "JiraService|JIRA comments will be created when an issue gets referenced in a commit." +msgstr "" + +msgid "JiraService|JIRA comments will be created when an issue gets referenced in a merge request." +msgstr "" + +msgid "JiraService|Jira issue tracker" +msgstr "" + +msgid "JiraService|Password or API token" +msgstr "" + +msgid "JiraService|Transition ID(s)" +msgstr "" + +msgid "JiraService|Use , or ; to separate multiple transition IDs" +msgstr "" + +msgid "JiraService|Use a password for server version and an API token for cloud version" +msgstr "" + +msgid "JiraService|Use a username for server version and an email for cloud version" +msgstr "" + +msgid "JiraService|Username or Email" +msgstr "" + +msgid "JiraService|Web URL" +msgstr "" + +msgid "JiraService|transition ids can have only numbers which can be split with , or ;" +msgstr "" + msgid "Job" msgstr "" @@ -4721,6 +4928,9 @@ msgstr "" msgid "Job is stuck. Check runners." msgstr "" +msgid "Job traces and artifacts" +msgstr "" + msgid "Job was retried" msgstr "" @@ -4829,6 +5039,9 @@ msgstr "" msgid "LFS" msgstr "" +msgid "LFS objects" +msgstr "" + msgid "LFSStatus|Disabled" msgstr "" @@ -5127,6 +5340,9 @@ msgstr "" msgid "Maximum job timeout" msgstr "" +msgid "Maximum job timeout has a value which could not be accepted" +msgstr "" + msgid "Maximum push size (MB)" msgstr "" @@ -5427,7 +5643,7 @@ msgstr "" msgid "Name:" msgstr "" -msgid "Naming, tags, avatar" +msgid "Naming, topics, avatar" msgstr "" msgid "Naming, visibility" @@ -5795,6 +6011,12 @@ msgstr "" msgid "OfSearchInADropdown|Filter" msgstr "" +msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source." +msgstr "" + +msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page." +msgstr "" + msgid "One more item" msgid_plural "%d more items" msgstr[0] "" @@ -5947,6 +6169,9 @@ msgstr "" msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key." msgstr "" +msgid "Path" +msgstr "" + msgid "Path, transfer, remove" msgstr "" @@ -6064,6 +6289,9 @@ msgstr "" msgid "Pipelines charts" msgstr "" +msgid "Pipelines emails" +msgstr "" + msgid "Pipelines for last month" msgstr "" @@ -6187,6 +6415,15 @@ msgstr "" msgid "Pipeline|with stages" msgstr "" +msgid "PivotalTrackerService|Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches." +msgstr "" + +msgid "PivotalTrackerService|Pivotal Tracker API token." +msgstr "" + +msgid "PivotalTrackerService|Project Management Software (Source Commits Endpoint)" +msgstr "" + msgid "Plain diff" msgstr "" @@ -6244,6 +6481,9 @@ msgstr "" msgid "Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately." msgstr "" +msgid "Please wait a moment, this page will automatically refresh when ready." +msgstr "" + msgid "Please wait while we import the repository for you. Refresh at will." msgstr "" @@ -6580,15 +6820,30 @@ msgstr "" msgid "Project Badges" msgstr "" +msgid "Project ID" +msgstr "" + msgid "Project URL" msgstr "" msgid "Project access must be granted explicitly to each user." msgstr "" +msgid "Project and wiki repositories" +msgstr "" + msgid "Project avatar" msgstr "" +msgid "Project cannot be shared with the group it is in or one of its ancestors." +msgstr "" + +msgid "Project configuration, including services" +msgstr "" + +msgid "Project description (optional)" +msgstr "" + msgid "Project details" msgstr "" @@ -6619,6 +6874,12 @@ msgstr "" msgid "Project slug" msgstr "" +msgid "Project uploads" +msgstr "" + +msgid "Project visibility level will be changed to match namespace rules when transferring to a group." +msgstr "" + msgid "Project:" msgstr "" @@ -6838,6 +7099,45 @@ msgstr "" msgid "Push to create a project" msgstr "" +msgid "PushoverService|%{user_name} deleted branch \"%{ref}\"." +msgstr "" + +msgid "PushoverService|%{user_name} push to branch \"%{ref}\"." +msgstr "" + +msgid "PushoverService|%{user_name} pushed new branch \"%{ref}\"." +msgstr "" + +msgid "PushoverService|High Priority" +msgstr "" + +msgid "PushoverService|Leave blank for all active devices" +msgstr "" + +msgid "PushoverService|Low Priority" +msgstr "" + +msgid "PushoverService|Lowest Priority" +msgstr "" + +msgid "PushoverService|Normal Priority" +msgstr "" + +msgid "PushoverService|Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop." +msgstr "" + +msgid "PushoverService|See project %{project_full_name}" +msgstr "" + +msgid "PushoverService|Total commits count: %{total_commits_count}" +msgstr "" + +msgid "PushoverService|Your application key" +msgstr "" + +msgid "PushoverService|Your user key" +msgstr "" + msgid "Quick actions can be used in the issues description and comment boxes." msgstr "" @@ -6924,6 +7224,9 @@ msgstr "" msgid "Remind later" msgstr "" +msgid "Remote object has no absolute path." +msgstr "" + msgid "Remove" msgstr "" @@ -6933,6 +7236,9 @@ msgstr "" msgid "Remove avatar" msgstr "" +msgid "Remove fork relationship" +msgstr "" + msgid "Remove group" msgstr "" @@ -6951,9 +7257,15 @@ msgstr "" msgid "Removed group can not be restored!" msgstr "" +msgid "Removed projects cannot be restored!" +msgstr "" + msgid "Removing group will cause all child projects and resources to be removed." msgstr "" +msgid "Removing the project will delete its repository and all related resources including issues, merge requests etc." +msgstr "" + msgid "Rename" msgstr "" @@ -7142,6 +7454,9 @@ msgstr "" msgid "Revoked personal access token %{personal_access_token_name}!" msgstr "" +msgid "Run housekeeping" +msgstr "" + msgid "Run untagged jobs" msgstr "" @@ -7196,6 +7511,9 @@ msgstr "" msgid "Running…" msgstr "" +msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects." +msgstr "" + msgid "SSH Keys" msgstr "" @@ -7235,6 +7553,9 @@ msgstr "" msgid "Save variables" msgstr "" +msgid "Saving project." +msgstr "" + msgid "Schedule a new pipeline" msgstr "" @@ -7358,6 +7679,9 @@ msgstr "" msgid "Select a namespace to fork the project" msgstr "" +msgid "Select a new namespace" +msgstr "" + msgid "Select a timezone" msgstr "" @@ -7412,6 +7736,9 @@ msgstr "" msgid "Sep" msgstr "" +msgid "Separate topics with commas." +msgstr "" + msgid "September" msgstr "" @@ -8253,6 +8580,12 @@ msgstr "" msgid "The file has been successfully deleted." msgstr "" +msgid "The following items will NOT be exported:" +msgstr "" + +msgid "The following items will be exported:" +msgstr "" + msgid "The fork relationship has been removed." msgstr "" @@ -8307,6 +8640,9 @@ msgstr "" msgid "The name %{entryName} is already taken in this directory." msgstr "" +msgid "The number of times an upload record could not find its file" +msgstr "" + msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest." msgstr "" @@ -8334,12 +8670,18 @@ msgstr "" msgid "The project can be accessed without any authentication." msgstr "" +msgid "The project is still being deleted. Please try again later." +msgstr "" + msgid "The project was successfully forked." msgstr "" msgid "The project was successfully imported." msgstr "" +msgid "The remote mirror took to long to complete." +msgstr "" + msgid "The remote repository is being updated..." msgstr "" @@ -8439,6 +8781,9 @@ msgstr "" msgid "There are no unstaged changes" msgstr "" +msgid "There is already a repository with that name on disk" +msgstr "" + msgid "There was an error loading users activity calendar." msgstr "" @@ -8484,6 +8829,9 @@ msgstr "" msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area." msgstr "" +msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention." +msgstr "" + msgid "This application was created by %{link_to_owner}." msgstr "" @@ -8532,6 +8880,9 @@ msgstr "" msgid "This group does not provide any group Runners yet." msgstr "" +msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed." +msgstr "" + msgid "This is a confidential issue." msgstr "" @@ -8676,6 +9027,9 @@ msgstr "" msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches." msgstr "" +msgid "This will remove the fork relationship to source project" +msgstr "" + msgid "Time before an issue gets scheduled" msgstr "" @@ -8977,6 +9331,9 @@ msgstr "" msgid "Too many changes to show." msgstr "" +msgid "Topics" +msgstr "" + msgid "Total Time" msgstr "" @@ -8989,6 +9346,9 @@ msgstr "" msgid "Track time with quick actions" msgstr "" +msgid "Transfer project" +msgstr "" + msgid "Tree view" msgstr "" @@ -9049,6 +9409,12 @@ msgstr "" msgid "Unable to schedule a pipeline to run immediately" msgstr "" +msgid "Unarchive project" +msgstr "" + +msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>" +msgstr "" + msgid "Unblock" msgstr "" @@ -9058,6 +9424,12 @@ msgstr "" msgid "Unfortunately, your email message to GitLab could not be processed." msgstr "" +msgid "Unknown encryption strategy: %{encrypted_strategy}!" +msgstr "" + +msgid "Unknown format" +msgstr "" + msgid "Unlock" msgstr "" @@ -9130,7 +9502,7 @@ msgstr "" msgid "Update your group name, description, avatar, and visibility." msgstr "" -msgid "Update your project name, tags, description and avatar." +msgid "Update your project name, topics, description and avatar." msgstr "" msgid "Updating" @@ -9154,6 +9526,9 @@ msgstr "" msgid "Upload file" msgstr "" +msgid "Upload file does not exist" +msgstr "" + msgid "Upload object map" msgstr "" @@ -9319,6 +9694,9 @@ msgstr "" msgid "Users were successfully added." msgstr "" +msgid "Using required encryption strategy when encrypted field is missing!" +msgstr "" + msgid "Validate" msgstr "" @@ -9454,6 +9832,9 @@ msgstr "" msgid "Web terminal" msgstr "" +msgid "Webhooks" +msgstr "" + msgid "Webhooks Help" msgstr "" @@ -9735,6 +10116,9 @@ msgstr "" msgid "You can only merge once the items above are resolved" msgstr "" +msgid "You can only transfer the project to namespaces you manage." +msgstr "" + msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgstr "" @@ -9825,6 +10209,9 @@ msgstr "" msgid "You will lose all the unstaged changes you've made in this project. This action cannot be undone." msgstr "" +msgid "You will need to update your local repositories to point to the new location." +msgstr "" + msgid "You will not get any notifications via email" msgstr "" @@ -9891,6 +10278,9 @@ msgstr "" msgid "Your Todos" msgstr "" +msgid "Your U2F device did not send a valid JSON response." +msgstr "" + msgid "Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left." msgstr "" @@ -9927,6 +10317,9 @@ msgstr "" msgid "Your comment will not be visible to the public." msgstr "" +msgid "Your deployment services will be broken, you will need to manually fix the services after renaming." +msgstr "" + msgid "Your device was successfully set up! Give it a name and register it with the GitLab server." msgstr "" @@ -9969,6 +10362,12 @@ msgstr "" msgid "allowed to fail" msgstr "" +msgid "already being used for another group or project milestone." +msgstr "" + +msgid "already shared with this group" +msgstr "" + msgid "among other things" msgstr "" @@ -9981,6 +10380,15 @@ msgstr "" msgid "branch name" msgstr "" +msgid "cannot be changed if a personal project has container registry tags." +msgstr "" + +msgid "cannot be enabled unless all domains have TLS certificates" +msgstr "" + +msgid "cannot include leading slash or directory traversal." +msgstr "" + msgid "commented on %{link_to_project}" msgstr "" @@ -10024,9 +10432,15 @@ msgstr[1] "" msgid "done" msgstr "" +msgid "element is not a hierarchy" +msgstr "" + msgid "enabled" msgstr "" +msgid "encrypted: needs to be a :required, :optional or :migrating!" +msgstr "" + msgid "error" msgstr "" @@ -10060,6 +10474,9 @@ msgstr "" msgid "group" msgstr "" +msgid "has already been taken" +msgstr "" + msgid "here" msgstr "" @@ -10087,6 +10504,9 @@ msgstr "" msgid "is not a valid X509 certificate." msgstr "" +msgid "is not an email you own" +msgstr "" + msgid "issue boards" msgstr "" @@ -10119,6 +10539,9 @@ msgid_plural "merge requests" msgstr[0] "" msgstr[1] "" +msgid "milestone should belong either to a project or a group." +msgstr "" + msgid "missing" msgstr "" @@ -10326,9 +10749,15 @@ msgstr "" msgid "mrWidget|to be merged automatically when the pipeline succeeds" msgstr "" +msgid "must be greater than start date" +msgstr "" + msgid "n/a" msgstr "" +msgid "needs to be beetween 10 minutes and 1 month" +msgstr "" + msgid "new merge request" msgstr "" @@ -10416,6 +10845,9 @@ msgstr "" msgid "source diff" msgstr "" +msgid "specified top is not part of the tree" +msgstr "" + msgid "spendCommand|%{slash_command} will update the sum of the time spent." msgstr "" diff --git a/qa/Dockerfile b/qa/Dockerfile index ca7f9accb70..ae3ec2acfdf 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -30,7 +30,7 @@ RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09. # RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add - RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list -RUN apt-get update -q && apt-get install -y google-chrome-stable && apt-get clean +RUN apt-get update -q && apt-get install -y --allow-unauthenticated google-chrome-stable && apt-get clean ## # Install chromedriver to make it work with Selenium diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index c9b0db6a272..9d3d42fb6ae 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -49,7 +49,7 @@ GEM mini_portile2 (2.4.0) minitest (5.11.1) netrc (0.11.0) - nokogiri (1.10.1) + nokogiri (1.10.2) mini_portile2 (~> 2.4.0) pry (0.11.3) coderay (~> 1.1.0) diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index 578f097e2dc..6dffbac5694 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -4,27 +4,21 @@ module QA module Settings class Advanced < Page::Base view 'app/views/projects/edit.html.haml' do - element :project_path_field, 'text_field :path' # rubocop:disable QA/ElementWithPattern - element :project_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern - element :rename_project_button, "submit 'Rename project'" # rubocop:disable QA/ElementWithPattern + element :project_path_field + element :change_path_button end - def rename_to(path) - fill_project_name(path) + def update_project_path_to(path) fill_project_path(path) - rename_project! + click_change_path_button end def fill_project_path(path) - fill_in :project_path, with: path + fill_element :project_path_field, path end - def fill_project_name(name) - fill_in :project_name, with: name - end - - def rename_project! - click_on 'Rename project' + def click_change_path_button + click_element :change_path_button end end end diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb index f3b217677f2..233e681e0df 100644 --- a/qa/qa/page/project/settings/common.rb +++ b/qa/qa/page/project/settings/common.rb @@ -4,14 +4,6 @@ module QA module Settings module Common include QA::Page::Settings::Common - - def self.included(base) - base.class_eval do - view 'app/views/projects/edit.html.haml' do - element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'" # rubocop:disable QA/ElementWithPattern - end - end - end end end end diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index d8cf1d49dd2..cf464e25ca5 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -9,6 +9,24 @@ module QA element :advanced_settings end + view 'app/views/projects/settings/_general.html.haml' do + element :project_name_field + element :save_naming_topics_avatar_button + end + + def rename_project_to(name) + fill_project_name(name) + click_save_changes + end + + def fill_project_name(name) + fill_element :project_name_field, name + end + + def click_save_changes + click_element :save_naming_topics_avatar_button + end + def expand_advanced_settings(&block) expand_section(:advanced_settings) do Advanced.perform(&block) diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index 741821ddf8c..5c86c102105 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -69,7 +69,7 @@ describe QA::Specs::Runner do subject { described_class.new } - it 'it includes default args and excludes the skip_signup_disabled tag' do + it 'includes default args and excludes the skip_signup_disabled tag' do expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~skip_signup_disabled', *described_class::DEFAULT_TEST_PATH_ARGS]) subject.perform @@ -83,7 +83,7 @@ describe QA::Specs::Runner do subject { described_class.new } - it 'it includes default args and excludes the requires_git_protocol_v2 tag' do + it 'includes default args and excludes the requires_git_protocol_v2 tag' do expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS]) subject.perform diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 70c34f8640b..0d8c26a2ee9 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -62,4 +62,10 @@ FactoryBot.define do project_key: 'jira-key' ) end + + factory :hipchat_service do + project + type 'HipchatService' + token 'test_token' + end end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index 25ed3bdc88e..ce780789f5a 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -73,22 +73,26 @@ describe 'Admin::Hooks' do end describe 'Remove existing hook', :js do + let(:hook_url) { generate(:url) } + before do - create(:system_hook) + create(:system_hook, url: hook_url) end context 'removes existing hook' do it 'from hooks list page' do visit admin_hooks_path - expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1) + accept_confirm { click_link 'Remove' } + expect(page).not_to have_content(hook_url) end it 'from hook edit page' do visit admin_hooks_path click_link 'Edit' - expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1) + accept_confirm { click_link 'Remove' } + expect(page).not_to have_content(hook_url) end end end diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb index 29442a58ea4..586e2e33112 100644 --- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb +++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb @@ -138,7 +138,7 @@ describe "User adds a comment on a commit", :js do click_button("Comment") end - page.within(".diff-file:nth-of-type(1) .notes_content.parallel.old") do + page.within(".diff-file:nth-of-type(1) .notes-content.parallel.old") do expect(page).to have_content(old_comment) end @@ -152,7 +152,7 @@ describe "User adds a comment on a commit", :js do wait_for_requests - expect(all(".diff-file:nth-of-type(1) .notes_content.parallel.new")[1].text).to have_content(new_comment) + expect(all(".diff-file:nth-of-type(1) .notes-content.parallel.new")[1].text).to have_content(new_comment) end end diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 50ba67f0ffc..f26941ab567 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -62,8 +62,9 @@ describe 'Projects > Members > User requests access', :js do accept_confirm { click_link 'Withdraw Access Request' } - expect(project.requesters.exists?(user_id: user)).to be_falsey expect(page).to have_content 'Your access request to the project has been withdrawn.' + expect(page).not_to have_content 'Withdraw Access Request' + expect(page).to have_content 'Request Access' end def open_project_settings_menu diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb index 65b597da269..1a13fe03a67 100644 --- a/spec/features/projects/services/disable_triggers_spec.rb +++ b/spec/features/projects/services/disable_triggers_spec.rb @@ -14,11 +14,10 @@ describe 'Disable individual triggers' do end context 'service has multiple supported events' do - let(:service_name) { 'JIRA' } + let(:service_name) { 'HipChat' } it 'shows trigger checkboxes' do - event_count = JiraService.supported_events.count - expect(event_count).to be > 1 + event_count = HipchatService.supported_events.count expect(page).to have_content "Trigger" expect(page).to have_css(checkbox_selector, count: event_count) diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb new file mode 100644 index 00000000000..d6b69a5bd68 --- /dev/null +++ b/spec/features/projects/services/user_activates_hipchat_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'User activates HipChat' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('HipChat') + end + + context 'with standart settings' do + it 'activates service' do + check('Active') + fill_in('Room', with: 'gitlab') + fill_in('Token', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('HipChat activated.') + end + end + + context 'with custom settings' do + it 'activates service' do + check('Active') + fill_in('Room', with: 'gitlab_custom') + fill_in('Token', with: 'secretCustom') + fill_in('Server', with: 'https://chat.example.com') + click_button('Save') + + expect(page).to have_content('HipChat activated.') + end + end +end diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb index b0a838a7d2b..e9c8cf0fe34 100644 --- a/spec/features/projects/services/user_views_services_spec.rb +++ b/spec/features/projects/services/user_views_services_spec.rb @@ -14,6 +14,7 @@ describe 'User views services' do it 'shows the list of available services' do expect(page).to have_content('Project services') expect(page).to have_content('Campfire') + expect(page).to have_content('HipChat') expect(page).to have_content('Assembla') expect(page).to have_content('Pushover') expect(page).to have_content('Atlassian Bamboo') @@ -21,7 +22,5 @@ describe 'User views services' do expect(page).to have_content('Asana') expect(page).to have_content('Irker (IRC gateway)') expect(page).to have_content('Packagist') - expect(page).to have_content('Mattermost') - expect(page).to have_content('Slack') end end diff --git a/spec/features/projects/settings/user_renames_a_project_spec.rb b/spec/features/projects/settings/user_renames_a_project_spec.rb index 64c9af4b706..d3979b79910 100644 --- a/spec/features/projects/settings/user_renames_a_project_spec.rb +++ b/spec/features/projects/settings/user_renames_a_project_spec.rb @@ -9,24 +9,33 @@ describe 'Projects > Settings > User renames a project' do visit edit_project_path(project) end - def rename_project(project, name: nil, path: nil) - fill_in('project_name', with: name) if name - fill_in('Path', with: path) if path - click_button('Rename project') + def change_path(project, path) + within('.advanced-settings') do + fill_in('Path', with: path) + click_button('Change path') + end + project.reload wait_for_edit_project_page_reload + end + + def change_name(project, name) + within('.general-settings') do + fill_in('Project name', with: name) + click_button('Save changes') + end project.reload + wait_for_edit_project_page_reload end def wait_for_edit_project_page_reload - expect(find('.project-edit-container')).to have_content('Rename repository') + expect(find('.advanced-settings')).to have_content('Change path') end context 'with invalid characters' do - it 'shows errors for invalid project path/name' do - rename_project(project, name: 'foo&bar', path: 'foo&bar') - expect(page).to have_field 'Project name', with: 'foo&bar' + it 'shows errors for invalid project path' do + change_path(project, 'foo&bar') + expect(page).to have_field 'Path', with: 'foo&bar' - expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'" end end @@ -42,13 +51,13 @@ describe 'Projects > Settings > User renames a project' do context 'when changing project name' do it 'renames the repository' do - rename_project(project, name: 'bar') + change_name(project, 'bar') expect(find('.breadcrumbs')).to have_content(project.name) end context 'with emojis' do it 'shows error for invalid project name' do - rename_project(project, name: '🚀 foo bar ☁️') + change_name(project, '🚀 foo bar ☁️') expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️' expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'." end @@ -67,7 +76,7 @@ describe 'Projects > Settings > User renames a project' do end it 'the project is accessible via the new path' do - rename_project(project, path: 'bar') + change_path(project, 'bar') new_path = namespace_project_path(project.namespace, 'bar') visit new_path @@ -77,7 +86,7 @@ describe 'Projects > Settings > User renames a project' do it 'the project is accessible via a redirect from the old path' do old_path = project_path(project) - rename_project(project, path: 'bar') + change_path(project, 'bar') new_path = namespace_project_path(project.namespace, 'bar') visit old_path @@ -88,7 +97,7 @@ describe 'Projects > Settings > User renames a project' do context 'and a new project is added with the same path' do it 'overrides the redirect' do old_path = project_path(project) - rename_project(project, path: 'bar') + change_path(project, 'bar') new_project = create(:project, namespace: user.namespace, path: 'gitlabhq', name: 'quz') visit old_path diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index dbf0d427976..ff4e6197746 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -373,6 +373,21 @@ describe 'Project' do end end + describe 'edit' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:path) { edit_project_path(project) } + + before do + project.add_maintainer(user) + sign_in(user) + visit path + end + + it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }, + { form: '.qa-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }] + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index ae9b65d1a39..ea02f36d9d0 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -246,26 +246,6 @@ describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do end end end - - describe "when two-factor authentication is disabled" do - let(:user) { create(:user) } - - before do - user = gitlab_sign_in(:user) - user.update_attribute(:otp_required_for_login, true) - visit profile_account_path - manage_two_factor_authentication - expect(page).to have_content("Your U2F device needs to be set up.") - register_u2f_device - end - - it "deletes u2f registrations" do - visit profile_two_factor_auth_path - expect do - accept_confirm { click_on "Disable" } - end.to change { U2fRegistration.count }.by(-1) - end - end end describe 'fallback code authentication' do diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index eea7bd87257..33a35069004 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -12,7 +12,7 @@ import { setTestTimeout } from 'helpers/timeout'; import $ from 'jquery'; describe('Clusters', () => { - setTestTimeout(500); + setTestTimeout(1000); let cluster; let mock; diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 3f331055a32..0878c1de095 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -23,14 +23,6 @@ describe('text_utility', () => { }); }); - describe('capitalizeFirstCharacter', () => { - it('returns string with first letter capitalized', () => { - expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab'); - expect(textUtils.highCountTrim(105)).toBe('99+'); - expect(textUtils.highCountTrim(100)).toBe('99+'); - }); - }); - describe('humanize', () => { it('should remove underscores and uppercase the first letter', () => { expect(textUtils.humanize('foo_bar')).toEqual('Foo bar'); @@ -57,9 +49,9 @@ describe('text_utility', () => { }); }); - describe('slugify', () => { - it('should remove accents and convert to lower case', () => { - expect(textUtils.slugify('João')).toEqual('joão'); + describe('capitalizeFirstCharacter', () => { + it('returns string with first letter capitalized', () => { + expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab'); }); }); @@ -152,6 +144,12 @@ describe('text_utility', () => { }); }); + describe('slugifyWithUnderscore', () => { + it('should replaces whitespaces with underscore and convert to lower case', () => { + expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string'); + }); + }); + describe('truncateNamespace', () => { it(`should return the root namespace if the namespace only includes one level`, () => { expect(textUtils.truncateNamespace('a / b')).toBe('a'); diff --git a/spec/frontend/mr_popover/index_spec.js b/spec/frontend/mr_popover/index_spec.js new file mode 100644 index 00000000000..8c33e52a04b --- /dev/null +++ b/spec/frontend/mr_popover/index_spec.js @@ -0,0 +1,30 @@ +import * as createDefaultClient from '~/lib/graphql'; +import { setHTMLFixture } from '../helpers/fixtures'; +import initMRPopovers from '~/mr_popover/index'; + +createDefaultClient.default = jest.fn(); + +describe('initMRPopovers', () => { + let mr1; + let mr2; + + beforeEach(() => { + setHTMLFixture(` + <div id="one" class="gfm-merge_request">MR1</div> + <div id="two" class="gfm-merge_request">MR2</div> + `); + + mr1 = document.querySelector('#one'); + mr2 = document.querySelector('#two'); + + mr1.addEventListener = jest.fn(); + mr2.addEventListener = jest.fn(); + }); + + it('does not add the same event listener twice', () => { + initMRPopovers([mr1, mr1, mr2]); + + expect(mr1.addEventListener).toHaveBeenCalledTimes(1); + expect(mr2.addEventListener).toHaveBeenCalledTimes(1); + }); +}); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 3ce69bc3c20..1aabf3c2132 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -75,6 +75,14 @@ describe('diffs/components/app', () => { expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); }); + it('does not add container-limiting classes when isFluidLayout', () => { + createComponent({ isFluidLayout: true }, ({ state }) => { + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + it('displays loading icon on loading', () => { createComponent({}, ({ state }) => { state.diffs.isLoading = true; diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 3d2c617e479..394e3343be6 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -670,7 +670,7 @@ describe('Notes', function() { done(); }) .catch(done.fail); - }); + }, 2000); }); describe('postComment with Slash commands', () => { diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ed557ffd4e3..54369ff75f4 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -223,6 +223,7 @@ project: - packagist_service - pivotaltracker_service - prometheus_service +- hipchat_service - flowdock_service - assembla_service - asana_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 773651dd226..4a7accc4c52 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6795,6 +6795,28 @@ "wiki_page_events": true }, { + "id": 93, + "title": "HipChat", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.219Z", + "updated_at": "2016-06-14T15:01:51.219Z", + "active": false, + "properties": { + "notify_only_broken_pipelines": true + }, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "pipeline_events": true, + "type": "HipchatService", + "category": "common", + "default": false, + "wiki_page_events": true + }, + { "id": 91, "title": "Flowdock", "project_id": 5, diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 2c1146ceff5..0a170a157fe 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -85,4 +85,12 @@ describe Gitlab::VisibilityLevel do .to eq(described_class::PRIVATE) end end + + describe '.valid_level?' do + it 'returns true when visibility is valid' do + expect(described_class.valid_level?(described_class::PRIVATE)).to be_truthy + expect(described_class.valid_level?(described_class::INTERNAL)).to be_truthy + expect(described_class.valid_level?(described_class::PUBLIC)).to be_truthy + end + end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb new file mode 100644 index 00000000000..fd9e33c1781 --- /dev/null +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -0,0 +1,410 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe HipchatService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:token) } + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:token) } + end + end + + describe "Execute" do + let(:hipchat) { described_class.new } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } + let(:project_name) { project.full_name.gsub(/\s/, '') } + let(:token) { 'verySecret' } + let(:server_url) { 'https://hipchat.example.com'} + let(:push_sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + before do + allow(hipchat).to receive_messages( + project_id: project.id, + project: project, + room: 123456, + server: server_url, + token: token + ) + WebMock.stub_request(:post, api_url) + end + + it 'tests and return errors' do + allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room') + result = hipchat.test(push_sample_data) + + expect(result[:success]).to be_falsey + expect(result[:result].to_s).to eq('no such room') + end + + it 'uses v1 if version is provided' do + allow(hipchat).to receive(:api_version).and_return('v1') + expect(HipChat::Client).to receive(:new).with( + token, + api_version: 'v1', + server_url: server_url + ).and_return(double(:hipchat_service).as_null_object) + hipchat.execute(push_sample_data) + end + + it 'uses v2 as the version when nothing is provided' do + allow(hipchat).to receive(:api_version).and_return('') + expect(HipChat::Client).to receive(:new).with( + token, + api_version: 'v2', + server_url: server_url + ).and_return(double(:hipchat_service).as_null_object) + hipchat.execute(push_sample_data) + end + + context 'push events' do + it "calls Hipchat API for push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "creates a push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + push_sample_data[:object_attributes] + branch = push_sample_data[:ref].gsub('refs/heads/', '') + expect(message).to include("#{user.name} pushed to branch " \ + "<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>") + end + end + + context 'tag_push events' do + let(:push_sample_data) do + Gitlab::DataBuilder::Push.build( + project, + user, + Gitlab::Git::BLANK_SHA, + '1' * 40, + 'refs/tags/test', + []) + end + + it "calls Hipchat API for tag push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "creates a tag push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + push_sample_data[:object_attributes] + expect(message).to eq("#{user.name} pushed new tag " \ + "<a href=\"#{project.web_url}/commits/test\">test</a> to " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>\n") + end + end + + context 'issue events' do + let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') } + let(:issue_service) { Issues::CreateService.new(project, user) } + let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } + + it "calls Hipchat API for issue events" do + hipchat.execute(issues_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "creates an issue message" do + message = hipchat.send(:create_issue_message, issues_sample_data) + + obj_attr = issues_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>Awesome issue</b>" \ + "<pre><strong>please</strong> fix</pre>") + end + end + + context 'merge request events' do + let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) } + let(:merge_service) { MergeRequests::CreateService.new(project, user) } + let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } + + it "calls Hipchat API for merge requests events" do + hipchat.execute(merge_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "creates a merge request message" do + message = hipchat.send(:create_merge_request_message, + merge_sample_data) + + obj_attr = merge_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>Awesome merge request</b>" \ + "<pre><strong>please</strong> fix</pre>") + end + end + + context "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, creator: user) } + + context 'when commit comment event triggered' do + let(:commit_note) do + create(:note_on_commit, author: user, project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit') + end + + it "calls Hipchat API for commit comment events" do + data = Gitlab::DataBuilder::Note.build(commit_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + commit_id = Commit.truncate_sha(data[:commit][:id]) + title = hipchat.send(:format_title, data[:commit][:message]) + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "#{title}" \ + "<pre>a comment on a commit</pre>") + end + end + + context 'when merge request comment event triggered' do + let(:merge_request) do + create(:merge_request, source_project: project, + target_project: project) + end + + let(:merge_request_note) do + create(:note_on_merge_request, noteable: merge_request, + project: project, + note: "merge request **note**") + end + + it "calls Hipchat API for merge request comment events" do + data = Gitlab::DataBuilder::Note.build(merge_request_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + merge_id = data[:merge_request]['iid'] + title = data[:merge_request]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>#{title}</b>" \ + "<pre>merge request <strong>note</strong></pre>") + end + end + + context 'when issue comment event triggered' do + let(:issue) { create(:issue, project: project) } + let(:issue_note) do + create(:note_on_issue, noteable: issue, project: project, + note: "issue **note**") + end + + it "calls Hipchat API for issue comment events" do + data = Gitlab::DataBuilder::Note.build(issue_note, user) + hipchat.execute(data) + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + issue_id = data[:issue]['iid'] + title = data[:issue]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>#{title}</b>" \ + "<pre>issue <strong>note</strong></pre>") + end + + context 'with confidential issue' do + before do + issue.update!(confidential: true) + end + + it 'calls Hipchat API with issue comment' do + data = Gitlab::DataBuilder::Note.build(issue_note, user) + hipchat.execute(data) + + message = hipchat.send(:create_message, data) + + expect(message).to include("<pre>issue <strong>note</strong></pre>") + end + end + end + + context 'when snippet comment event triggered' do + let(:snippet) { create(:project_snippet, project: project) } + let(:snippet_note) do + create(:note_on_project_snippet, noteable: snippet, + project: project, + note: "snippet note") + end + + it "calls Hipchat API for snippet comment events" do + data = Gitlab::DataBuilder::Note.build(snippet_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + snippet_id = data[:snippet]['id'] + title = data[:snippet]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>#{title}</b>" \ + "<pre>snippet note</pre>") + end + end + end + + context 'pipeline events' do + let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) } + let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } + + context 'for failed' do + before do + pipeline.drop + end + + it "calls Hipchat API" do + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "creates a build message" do + message = hipchat.__send__(:create_pipeline_message, data) + + project_url = project.web_url + project_name = project.full_name.gsub(/\s/, '') + pipeline_attributes = data[:object_attributes] + ref = pipeline_attributes[:ref] + ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + duration = pipeline_attributes[:duration] + user_name = data[:user][:name] + + expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \ + "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \ + "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \ + "by #{user_name} failed in #{duration} second(s)") + end + end + + context 'for succeeded' do + before do + pipeline.succeed + end + + it "calls Hipchat API" do + hipchat.notify_only_broken_pipelines = false + hipchat.execute(data) + expect(WebMock).to have_requested(:post, api_url).once + end + + it "notifies only broken" do + hipchat.notify_only_broken_pipelines = true + hipchat.execute(data) + expect(WebMock).not_to have_requested(:post, api_url).once + end + end + end + + context "#message_options" do + it "is set to the defaults" do + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' }) + end + + it "sets notify to true" do + allow(hipchat).to receive(:notify).and_return('1') + + expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' }) + end + + it "sets the color" do + allow(hipchat).to receive(:color).and_return('red') + + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' }) + end + + context 'with a successful build' do + it 'uses the green color' do + data = { object_kind: 'pipeline', + object_attributes: { status: 'success' } } + + expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' }) + end + end + + context 'with a failed build' do + it 'uses the red color' do + data = { object_kind: 'pipeline', + object_attributes: { status: 'failed' } } + + expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' }) + end + end + end + end + + context 'with UrlBlocker' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:hipchat) { create(:hipchat_service, project: project, properties: { room: 'test' }) } + let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } + + describe '#execute' do + before do + hipchat.server = 'http://localhost:9123' + end + + it 'raises UrlBlocker for localhost' do + expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original + expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7f8d2ff91fd..9f6a0b53281 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -44,6 +44,7 @@ describe Project do it { is_expected.to have_one(:pipelines_email_service) } it { is_expected.to have_one(:irker_service) } it { is_expected.to have_one(:pivotaltracker_service) } + it { is_expected.to have_one(:hipchat_service) } it { is_expected.to have_one(:flowdock_service) } it { is_expected.to have_one(:assembla_service) } it { is_expected.to have_one(:slack_slash_commands_service) } diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index cacdb0e0595..d5c85c11195 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -22,6 +22,7 @@ describe WikiPage do create_page('dir_1/dir_1_1/page_3', 'content') create_page('page_1', 'content') create_page('dir_1/page_2', 'content') + create_page('dir_2', 'page with dir name') create_page('dir_2/page_5', 'content') create_page('page_6', 'content') create_page('dir_2/page_4', 'content') @@ -29,6 +30,7 @@ describe WikiPage do let(:page_1) { wiki.find_page('page_1') } let(:page_6) { wiki.find_page('page_6') } + let(:page_dir_2) { wiki.find_page('dir_2') } let(:dir_1) do WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) @@ -44,7 +46,7 @@ describe WikiPage do context 'sort by title' do let(:grouped_entries) { described_class.group_by_directory(wiki.pages) } - let(:expected_grouped_entries) { [dir_1_1, dir_1, dir_2, page_1, page_6] } + let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] } it 'returns an array with pages and directories' do grouped_entries.each_with_index do |page_or_dir, i| @@ -59,7 +61,7 @@ describe WikiPage do context 'sort by created_at' do let(:grouped_entries) { described_class.group_by_directory(wiki.pages(sort: 'created_at')) } - let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, dir_2, page_6] } + let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] } it 'returns an array with pages and directories' do grouped_entries.each_with_index do |page_or_dir, i| @@ -73,7 +75,7 @@ describe WikiPage do end it 'returns an array with retained order with directories at the top' do - expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6'] + expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6'] grouped_entries = described_class.group_by_directory(wiki.pages) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 0d46463312b..26158231444 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe API::Pipelines do @@ -493,7 +495,7 @@ describe API::Pipelines do context 'pipeline created is not created by the developer user' do let(:api_user) { create(:user) } - it 'should not return pipeline variables' do + it 'does not return pipeline variables' do subject expect(response).to have_gitlab_http_status(403) @@ -502,7 +504,7 @@ describe API::Pipelines do end context 'user is not a project member' do - it 'should not return pipeline variables' do + it 'does not return pipeline variables' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member) expect(response).to have_gitlab_http_status(404) diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb index 494c5892210..9e1ae5e8742 100644 --- a/spec/services/clusters/applications/install_service_spec.rb +++ b/spec/services/clusters/applications/install_service_spec.rb @@ -60,7 +60,7 @@ describe Clusters::Applications::InstallService do let(:error) { StandardError.new('something bad happened') } before do - expect(application).to receive(:make_installing!).once.and_raise(error) + expect(helm_client).to receive(:install).with(install_command).and_raise(error) end include_examples 'logs kubernetes errors' do @@ -70,12 +70,10 @@ describe Clusters::Applications::InstallService do end it 'make the application errored' do - expect(helm_client).not_to receive(:install) - service.execute expect(application).to be_errored - expect(application.status_reason).to eq("Can't start installation process.") + expect(application.status_reason).to eq('Failed to install.') end end end diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb index 10b1379a127..3ebe0540837 100644 --- a/spec/services/clusters/applications/patch_service_spec.rb +++ b/spec/services/clusters/applications/patch_service_spec.rb @@ -66,16 +66,14 @@ describe Clusters::Applications::PatchService do end before do - expect(application).to receive(:make_updating!).once.and_raise(error) + expect(helm_client).to receive(:update).with(update_command).and_raise(error) end it 'make the application errored' do - expect(helm_client).not_to receive(:update) - service.execute expect(application).to be_update_errored - expect(application.status_reason).to eq("Can't start update process.") + expect(application.status_reason).to eq('Failed to update.') end end end diff --git a/spec/services/clusters/applications/upgrade_service_spec.rb b/spec/services/clusters/applications/upgrade_service_spec.rb index dd2e6e94e4f..a80b1d9127c 100644 --- a/spec/services/clusters/applications/upgrade_service_spec.rb +++ b/spec/services/clusters/applications/upgrade_service_spec.rb @@ -60,7 +60,7 @@ describe Clusters::Applications::UpgradeService do let(:error) { StandardError.new('something bad happened') } before do - expect(application).to receive(:make_updating!).once.and_raise(error) + expect(helm_client).to receive(:update).with(install_command).and_raise(error) end include_examples 'logs kubernetes errors' do @@ -70,12 +70,10 @@ describe Clusters::Applications::UpgradeService do end it 'make the application errored' do - expect(helm_client).not_to receive(:update) - service.execute expect(application).to be_update_errored - expect(application.status_reason).to eq("Can't start upgrade process.") + expect(application.status_reason).to eq('Failed to upgrade.') end end end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 54836c9adbf..c5ff6cdbacd 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -90,6 +90,17 @@ describe Groups::CreateService, '#execute' do end end + describe "when visibility level is passed as a string" do + let(:service) { described_class.new(user, group_params) } + let(:group_params) { { path: 'group_path', visibility: 'public' } } + + it "assigns the correct visibility level" do + group = service.execute + + expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + describe 'creating a mattermost team' do let!(:params) { group_params.merge(create_chat_team: "true") } let!(:service) { described_class.new(user, params) } diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 48fc247f699..d8d2be87fd3 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -78,7 +78,7 @@ describe Users::ActivityService do let(:last_activity_on) { nil } it 'does not update last_activity_on' do - stub_exclusive_lease_taken("acitvity_service:#{user.id}", timeout: 1.minute.to_i) + stub_exclusive_lease_taken("activity_service:#{user.id}", timeout: 1.minute.to_i) expect { subject.execute }.not_to change(user, :last_activity_on) end diff --git a/spec/support/shared_examples/dirty_submit_form_shared_examples.rb b/spec/support/shared_examples/dirty_submit_form_shared_examples.rb index 52a2ee49495..4e45e2921e7 100644 --- a/spec/support/shared_examples/dirty_submit_form_shared_examples.rb +++ b/spec/support/shared_examples/dirty_submit_form_shared_examples.rb @@ -1,18 +1,17 @@ shared_examples 'dirty submit form' do |selector_args| selectors = selector_args.is_a?(Array) ? selector_args : [selector_args] - def expect_disabled_state(form, submit, is_disabled = true) + def expect_disabled_state(form, submit_selector, is_disabled = true) disabled_selector = is_disabled == true ? '[disabled]' : ':not([disabled])' - form.find(".js-dirty-submit#{disabled_selector}", match: :first) - - expect(submit.disabled?).to be is_disabled + form.find("#{submit_selector}#{disabled_selector}") end selectors.each do |selector| it "disables #{selector[:form]} submit until there are changes on #{selector[:input]}", :js do form = find(selector[:form]) - submit = form.first('.js-dirty-submit') + submit_selector = selector[:submit] || 'input[type="submit"]' + submit = form.first(submit_selector) input = form.first(selector[:input]) is_radio = input[:type] == 'radio' is_checkbox = input[:type] == 'checkbox' @@ -22,15 +21,14 @@ shared_examples 'dirty submit form' do |selector_args| original_checkable = input if is_checkbox expect(submit.disabled?).to be true - expect(input.checked?).to be false is_checkable ? input.click : input.set("#{original_value} changes") - expect_disabled_state(form, submit, false) + expect_disabled_state(form, submit_selector, false) is_checkable ? original_checkable.click : input.set(original_value) - expect_disabled_state(form, submit) + expect_disabled_state(form, submit_selector) end end end diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 75ad948e42c..d87e5fcaa88 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_visible_access_request(entity, user) - accept_confirm { click_on 'Grant access' } + click_on 'Grant access' expect_no_visible_access_request(entity, user) @@ -40,13 +40,11 @@ RSpec.shared_examples 'Maintainer manages access requests' do end def expect_visible_access_request(entity, user) - expect(entity.requesters.exists?(user_id: user)).to be_truthy expect(page).to have_content "Users requesting access to #{entity.name} 1" expect(page).to have_content user.name end def expect_no_visible_access_request(entity, user) - expect(entity.requesters.exists?(user_id: user)).to be_falsy expect(page).not_to have_content "Users requesting access to #{entity.name}" end end diff --git a/vendor/licenses.csv b/vendor/licenses.csv index de6e32cb998..0c52cb5a947 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -520,6 +520,7 @@ hashie-forbidden_attributes,0.1.1,MIT he,1.1.1,MIT health_check,2.6.0,MIT highlight.js,9.13.1,New BSD +hipchat,1.5.2,MIT hmac-drbg,1.0.1,MIT hoopy,0.1.4,MIT html-pipeline,2.8.4,MIT |