summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js4
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue38
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js19
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue49
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue81
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue250
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js25
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue149
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue78
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue48
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js (renamed from app/assets/javascripts/boards/models/label.js)4
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb1
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb16
-rw-r--r--app/helpers/import_helper.rb40
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/models/badge.rb51
-rw-r--r--app/models/badges/group_badge.rb5
-rw-r--r--app/models/badges/project_badge.rb15
-rw-r--r--app/models/clusters/applications/runner.rb5
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/project_services/asana_service.rb2
-rw-r--r--app/models/project_services/campfire_service.rb2
-rw-r--r--app/models/project_services/chat_notification_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb6
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb2
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/services/badges/base_service.rb11
-rw-r--r--app/services/badges/build_service.rb12
-rw-r--r--app/services/badges/create_service.rb10
-rw-r--r--app/services/badges/update_service.rb12
-rw-r--r--app/services/system_hooks_service.rb4
-rw-r--r--app/validators/url_placeholder_validator.rb32
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml4
-rw-r--r--app/views/admin/projects/show.html.haml6
-rw-r--r--app/views/admin/runners/show.html.haml6
-rw-r--r--app/views/admin/users/projects.html.haml4
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/invites/show.html.haml2
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml2
-rw-r--r--app/views/layouts/project.html.haml2
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.html.haml2
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml2
-rw-r--r--app/views/profiles/keys/index.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml6
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/imports/show.html.haml13
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/new.html.haml31
-rw-r--r--app/views/projects/pipelines/index.html.haml9
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/views/search/_filter.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml19
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb2
-rw-r--r--app/workers/git_garbage_collect_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb2
-rw-r--r--changelogs/unreleased/38587-pipelines-empty-state.yml5
-rw-r--r--changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml5
-rw-r--r--changelogs/unreleased/43829-update-ssh-addtion-text.yml5
-rw-r--r--changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml5
-rw-r--r--changelogs/unreleased/fj-41174-projects-groups-badges-api.yml5
-rw-r--r--changelogs/unreleased/issue_31081.yml5
-rw-r--r--changelogs/unreleased/kp-label-select-vue.yml5
-rw-r--r--changelogs/unreleased/oauth_generic_provider.yml4
-rw-r--r--changelogs/unreleased/sh-cleanup-after-git-gc.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml5
-rw-r--r--config.ru1
-rw-r--r--config/application.rb1
-rw-r--r--db/migrate/20180214093516_create_badges.rb17
-rw-r--r--db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb7
-rw-r--r--db/migrate/20180304204842_clean_commits_count_migration.rb14
-rw-r--r--db/migrate/20180305144721_add_privileged_to_runner.rb18
-rw-r--r--db/schema.rb19
-rw-r--r--doc/api/README.md2
-rw-r--r--doc/api/group_badges.md191
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/project_badges.md188
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/user/project/import/img/import_projects_from_repo_url.pngbin0 -> 150259 bytes
-rw-r--r--doc/user/project/import/index.md1
-rw-r--r--doc/user/project/import/repo_by_url.md12
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/badges.rb134
-rw-r--r--lib/api/entities.rb18
-rw-r--r--lib/api/helpers/badges_helpers.rb28
-rw-r--r--lib/api/runner.rb3
-rw-r--r--lib/gitlab/auth.rb30
-rw-r--r--lib/gitlab/auth/database/authentication.rb16
-rw-r--r--lib/gitlab/auth/ldap/authentication.rb10
-rw-r--r--lib/gitlab/auth/o_auth/authentication.rb21
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb17
-rw-r--r--lib/gitlab/auth/o_auth/user.rb2
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml5
-rw-r--r--lib/gitlab/import_export/relation_factory.rb3
-rw-r--r--lib/gitlab/middleware/read_only.rb83
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb86
-rw-r--r--lib/gitlab/middleware/release_env.rb14
-rw-r--r--lib/gitlab/string_placeholder_replacer.rb27
-rw-r--r--locale/gitlab.pot52
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb4
-rw-r--r--spec/factories/badge.rb14
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb8
-rw-r--r--spec/features/dashboard/issues_spec.rb8
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb4
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb8
-rw-r--r--spec/features/issues/move_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343092 -> 343087 bytes
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb39
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb6
-rw-r--r--spec/helpers/members_helper_spec.rb10
-rw-r--r--spec/helpers/todos_helper_spec.rb4
-rw-r--r--spec/javascripts/boards/board_card_spec.js2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js2
-rw-r--r--spec/javascripts/boards/issue_card_spec.js2
-rw-r--r--spec/javascripts/boards/issue_spec.js2
-rw-r--r--spec/javascripts/boards/list_spec.js2
-rw-r--r--spec/javascripts/boards/modal_store_spec.js2
-rw-r--r--spec/javascripts/pipelines/blank_state_spec.js29
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js28
-rw-r--r--spec/javascripts/pipelines/error_state_spec.js27
-rw-r--r--spec/javascripts/pipelines/nav_controls_spec.js84
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js677
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js81
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js82
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js84
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js42
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js36
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js39
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js42
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js74
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js94
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js49
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb8
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/project.json767
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml9
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb25
-rw-r--r--spec/lib/gitlab/middleware/release_env_spec.rb16
-rw-r--r--spec/lib/gitlab/string_placeholder_replacer_spec.rb38
-rw-r--r--spec/mailers/notify_spec.rb22
-rw-r--r--spec/models/badge_spec.rb94
-rw-r--r--spec/models/badges/group_badge_spec.rb11
-rw-r--r--spec/models/badges/project_badge_spec.rb43
-rw-r--r--spec/models/clusters/applications/runner_spec.rb30
-rw-r--r--spec/models/group_spec.rb1
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb4
-rw-r--r--spec/models/project_services/jira_service_spec.rb3
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb33
-rw-r--r--spec/models/project_wiki_spec.rb6
-rw-r--r--spec/requests/api/badges_spec.rb367
-rw-r--r--spec/requests/api/pages_domains_spec.rb2
-rw-r--r--spec/requests/api/runner_spec.rb4
-rw-r--r--spec/requests/git_http_spec.rb6
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb18
-rw-r--r--spec/services/system_hooks_service_spec.rb12
-rw-r--r--spec/services/system_note_service_spec.rb6
-rw-r--r--spec/validators/url_placeholder_validator_spec.rb39
-rw-r--r--spec/validators/url_validator_spec.rb46
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb54
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb2
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb6
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb2
-rw-r--r--vendor/project_templates/express.tar.gzbin5614 -> 5608 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25007 -> 25004 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50945 -> 50938 bytes
-rw-r--r--vendor/runner/values.yaml2
226 files changed, 4677 insertions, 1291 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 70f86a45043..a5c94a9e074 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -601,7 +601,7 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
- peek-performance_bar (1.3.0)
+ peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 417ac31fc86..81c89441424 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -12,7 +12,7 @@ $(() => {
const $container = $(container);
$container
- .find('.js-toggle-button .fa')
+ .find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
@@ -22,7 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
- e.target.classList.toggle('open');
+ e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 8e31f1865f0..8b34fe232c2 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -5,12 +5,12 @@ import Vue from 'vue';
import Flash from '~/flash';
import { __ } from '~/locale';
+import '~/vue_shared/models/label';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue';
-import './models/label';
import './models/list';
import './models/milestone';
import './models/assignee';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index ce19069f103..466a5b5d635 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -20,10 +20,6 @@
type: String,
required: true,
},
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
errorStateSvgPath: {
type: String,
required: true,
@@ -45,23 +41,14 @@
},
computed: {
- /**
- * Empty state is only rendered if after the first request we receive no pipelines.
- *
- * @return {Boolean}
- */
- shouldRenderEmptyState() {
- return !this.state.pipelines.length &&
- !this.isLoading &&
- this.hasMadeRequest &&
- !this.hasError;
- },
-
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -92,25 +79,22 @@
<div class="content-list pipelines">
<loading-icon
- label="Loading pipelines"
+ :label="s__('Pipelines|Loading Pipelines')"
size="3"
v-if="isLoading"
+ class="prepend-top-20"
/>
- <empty-state
- v-if="shouldRenderEmptyState"
- :help-page-path="helpPagePath"
- :empty-state-svg-path="emptyStateSvgPath"
- />
-
- <error-state
- v-if="shouldRenderErrorState"
- :error-state-svg-path="errorStateSvgPath"
+ <svg-blank-state
+ v-else-if="shouldRenderErrorState"
+ :svg-path="errorStateSvgPath"
+ :message="s__(`Pipelines|There was an error fetching the pipelines.
+ Try again in a few moments or contact your support team.`)"
/>
<div
class="table-holder"
- v-if="shouldRenderTable"
+ v-else-if="shouldRenderTable"
>
<pipelines-table-component
:pipelines="state.pipelines"
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
index 25dfa99ad9c..a84e2790680 100644
--- a/app/assets/javascripts/pages/projects/pipelines/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
+import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate);
@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
pipelinesComponent,
},
data() {
- const store = new PipelinesStore();
-
return {
- store,
+ store: new PipelinesStore(),
};
},
+ created() {
+ this.dataset = document.querySelector(this.$options.el).dataset;
+ },
render(createElement) {
return createElement('pipelines-component', {
props: {
store: this.store,
+ endpoint: this.dataset.endpoint,
+ helpPagePath: this.dataset.helpPagePath,
+ emptyStateSvgPath: this.dataset.emptyStateSvgPath,
+ errorStateSvgPath: this.dataset.errorStateSvgPath,
+ noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
+ autoDevopsPath: this.dataset.helpAutoDevopsPath,
+ newPipelinePath: this.dataset.newPipelinePath,
+ canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
+ hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
+ ciLintPath: this.dataset.ciLintPath,
+ resetCachePath: this.dataset.resetCachePath,
},
});
},
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
new file mode 100644
index 00000000000..8d3d6223d7b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -0,0 +1,32 @@
+<script>
+ export default {
+ name: 'PipelinesSvgState',
+ props: {
+ svgPath: {
+ type: String,
+ required: true,
+ },
+
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ <img :src="svgPath" />
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>{{ message }}</h4>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index dfaa2574091..10ac8c08bed 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,5 +1,6 @@
<script>
export default {
+ name: 'PipelinesEmptyState',
props: {
helpPagePath: {
type: String,
@@ -9,6 +10,10 @@
type: String,
required: true,
},
+ canSetCi: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
@@ -22,22 +27,36 @@
<div class="col-xs-12">
<div class="text-content">
- <h4 class="text-center">
- {{ s__("Pipelines|Build with confidence") }}
- </h4>
- <p>
- {{ s__(`Pipelines|Continous Integration can help
-catch bugs by running your tests automatically,
-while Continuous Deployment can help you deliver code to your product environment.`) }}
+
+ <template v-if="canSetCi">
+ <h4 class="text-center">
+ {{ s__('Pipelines|Build with confidence') }}
+ </h4>
+
+ <p>
+ {{ s__(`Pipelines|Continous Integration can help
+ catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver
+ code to your product environment.`) }}
+ </p>
+
+ <div class="text-center">
+ <a
+ :href="helpPagePath"
+ class="btn btn-primary js-get-started-pipelines"
+ >
+ {{ s__('Pipelines|Get started with Pipelines') }}
+ </a>
+ </div>
+ </template>
+
+ <p
+ v-else
+ class="text-center"
+ >
+ {{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
- <div class="text-center">
- <a
- :href="helpPagePath"
- class="btn btn-info"
- >
- {{ s__("Pipelines|Get started with Pipelines") }}
- </a>
- </div>
+
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
deleted file mode 100644
index 012853b201d..00000000000
--- a/app/assets/javascripts/pipelines/components/error_state.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- props: {
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="row empty-state js-pipelines-error-state">
- <div class="col-xs-12">
- <div class="svg-content">
- <img :src="errorStateSvgPath"/>
- </div>
- </div>
-
- <div class="col-xs-12 text-center">
- <div class="text-content">
- <h4>The API failed to fetch the pipelines.</h4>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index f31a91c3403..383ab51fe56 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,67 +1,52 @@
<script>
-export default {
- name: 'PipelineNavControls',
- props: {
- newPipelinePath: {
- type: String,
- required: true,
+ export default {
+ name: 'PipelineNavControls',
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
-
- hasCiEnabled: {
- type: Boolean,
- required: true,
- },
-
- helpPagePath: {
- type: String,
- required: true,
- },
-
- resetCachePath: {
- type: String,
- required: true,
- },
-
- ciLintPath: {
- type: String,
- required: true,
- },
-
- canCreatePipeline: {
- type: Boolean,
- required: true,
- },
- },
-};
+ };
</script>
<template>
<div class="nav-controls">
<a
- v-if="canCreatePipeline"
+ v-if="newPipelinePath"
:href="newPipelinePath"
- class="btn btn-create">
- Run Pipeline
- </a>
-
- <a
- v-if="!hasCiEnabled"
- :href="helpPagePath"
- class="btn btn-info">
- Get started with Pipelines
+ class="btn btn-create js-run-pipeline"
+ >
+ {{ s__('Pipelines|Run Pipeline') }}
</a>
<a
+ v-if="resetCachePath"
data-method="post"
- rel="nofollow"
:href="resetCachePath"
- class="btn btn-default">
- Clear runner caches
+ class="btn btn-default js-clear-cache"
+ >
+ {{ s__('Pipelines|Clear Runner Caches') }}
</a>
<a
+ v-if="ciLintPath"
:href="ciLintPath"
- class="btn btn-default">
- CI Lint
+ class="btn btn-default js-ci-lint"
+ >
+ {{ s__('Pipelines|CI Lint') }}
</a>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 90930d5ff44..6e5ee68eeb1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,12 +1,12 @@
<script>
import _ from 'underscore';
+ import { __, sprintf, s__ } from '../../locale';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
- import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
- import navigationControls from './nav_controls.vue';
+ import TablePagination from '../../vue_shared/components/table_pagination.vue';
+ import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
+ import NavigationControls from './nav_controls.vue';
import {
- convertPermissionToBoolean,
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
@@ -14,9 +14,9 @@
export default {
components: {
- tablePagination,
- navigationTabs,
- navigationControls,
+ TablePagination,
+ NavigationTabs,
+ NavigationControls,
},
mixins: [
pipelinesMixin,
@@ -36,111 +36,186 @@
required: false,
default: 'root',
},
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noPipelinesSvgPath: {
+ type: String,
+ required: true,
+ },
+ autoDevopsPath: {
+ type: String,
+ required: true,
+ },
+ hasGitlabCi: {
+ type: Boolean,
+ required: true,
+ },
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
- const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
-
return {
- endpoint: pipelinesData.endpoint,
- helpPagePath: pipelinesData.helpPagePath,
- emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
- errorStateSvgPath: pipelinesData.errorStateSvgPath,
- autoDevopsPath: pipelinesData.helpAutoDevopsPath,
- newPipelinePath: pipelinesData.newPipelinePath,
- canCreatePipeline: pipelinesData.canCreatePipeline,
- hasCi: pipelinesData.hasCi,
- ciLintPath: pipelinesData.ciLintPath,
- resetCachePath: pipelinesData.resetCachePath,
+ // Start with loading state to avoid a glitch when the empty state will be rendered
+ isLoading: true,
state: this.store.state,
scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1',
requestData: {},
};
},
- computed: {
- canCreatePipelineParsed() {
- return convertPermissionToBoolean(this.canCreatePipeline);
- },
+ stateMap: {
+ // with tabs
+ loading: 'loading',
+ tableList: 'tableList',
+ error: 'error',
+ emptyTab: 'emptyTab',
+ // without tabs
+ emptyState: 'emptyState',
+ },
+ scopes: {
+ all: 'all',
+ pending: 'pending',
+ running: 'running',
+ finished: 'finished',
+ branches: 'branches',
+ tags: 'tags',
+ },
+ computed: {
/**
- * The empty state should only be rendered when the request is made to fetch all pipelines
- * and none is returned.
- *
- * @return {Boolean}
- */
- shouldRenderEmptyState() {
- return !this.isLoading &&
- !this.hasError &&
- this.hasMadeRequest &&
- !this.state.pipelines.length &&
- (this.scope === 'all' || this.scope === null);
+ * `hasGitlabCi` handles both internal and external CI.
+ * The order on which the checks are made in this method is
+ * important to guarantee we handle all the corner cases.
+ */
+ stateToRender() {
+ const { stateMap } = this.$options;
+
+ if (this.isLoading) {
+ return stateMap.loading;
+ }
+
+ if (this.hasError) {
+ return stateMap.error;
+ }
+
+ if (this.state.pipelines.length) {
+ return stateMap.tableList;
+ }
+
+ if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
+ return stateMap.emptyTab;
+ }
+
+ return stateMap.emptyState;
},
/**
- * When a specific scope does not have pipelines we render a message.
- *
- * @return {Boolean}
+ * Tabs are rendered in all states except empty state.
+ * They are not rendered before the first request to avoid a flicker on first load.
*/
- shouldRenderNoPipelinesMessage() {
- return !this.isLoading &&
- !this.hasError &&
- !this.state.pipelines.length &&
- this.scope !== 'all' &&
- this.scope !== null;
+ shouldRenderTabs() {
+ const { stateMap } = this.$options;
+ return this.hasMadeRequest &&
+ [
+ stateMap.loading,
+ stateMap.tableList,
+ stateMap.error,
+ stateMap.emptyTab,
+ ].includes(this.stateToRender);
},
- shouldRenderTable() {
- return !this.hasError &&
- !this.isLoading && this.state.pipelines.length;
+ shouldRenderButtons() {
+ return (this.newPipelinePath ||
+ this.resetCachePath ||
+ this.ciLintPath) && this.shouldRenderTabs;
},
- /**
- * Pagination should only be rendered when there is more than one page.
- *
- * @return {Boolean}
- */
+
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
- hasCiEnabled() {
- return this.hasCi !== undefined;
+
+ emptyTabMessage() {
+ const { scopes } = this.$options;
+ const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
+
+ if (possibleScopes.includes(this.scope)) {
+ return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
+ scope: this.scope,
+ });
+ }
+
+ return s__('Pipelines|There are currently no pipelines.');
},
tabs() {
const { count } = this.state;
+ const { scopes } = this.$options;
+
return [
{
- name: 'All',
- scope: 'all',
+ name: __('All'),
+ scope: scopes.all,
count: count.all,
isActive: this.scope === 'all',
},
{
- name: 'Pending',
- scope: 'pending',
+ name: __('Pending'),
+ scope: scopes.pending,
count: count.pending,
isActive: this.scope === 'pending',
},
{
- name: 'Running',
- scope: 'running',
+ name: __('Running'),
+ scope: scopes.running,
count: count.running,
isActive: this.scope === 'running',
},
{
- name: 'Finished',
- scope: 'finished',
+ name: __('Finished'),
+ scope: scopes.finished,
count: count.finished,
isActive: this.scope === 'finished',
},
{
- name: 'Branches',
- scope: 'branches',
+ name: __('Branches'),
+ scope: scopes.branches,
isActive: this.scope === 'branches',
},
{
- name: 'Tags',
- scope: 'tags',
+ name: __('Tags'),
+ scope: scopes.tags,
isActive: this.scope === 'tags',
},
];
@@ -187,7 +262,7 @@
this.errorCallback();
// restart polling
- this.poll.restart();
+ this.poll.restart({ data: this.requestData });
});
},
},
@@ -197,69 +272,70 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
- v-if="!shouldRenderEmptyState"
+ v-if="shouldRenderTabs || shouldRenderButtons"
>
<div class="fade-left">
<i
class="fa fa-angle-left"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</div>
<navigation-tabs
+ v-if="shouldRenderTabs"
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="pipelines"
/>
<navigation-controls
+ v-if="shouldRenderButtons"
:new-pipeline-path="newPipelinePath"
- :has-ci-enabled="hasCiEnabled"
- :help-page-path="helpPagePath"
:reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath"
- :can-create-pipeline="canCreatePipelineParsed "
/>
</div>
<div class="content-list pipelines">
<loading-icon
- label="Loading Pipelines"
+ v-if="stateToRender === $options.stateMap.loading"
+ :label="s__('Pipelines|Loading Pipelines')"
size="3"
- v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
- v-if="shouldRenderEmptyState"
+ v-else-if="stateToRender === $options.stateMap.emptyState"
:help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath"
+ :can-set-ci="canCreatePipeline"
/>
- <error-state
- v-if="shouldRenderErrorState"
- :error-state-svg-path="errorStateSvgPath"
+ <svg-blank-state
+ v-else-if="stateToRender === $options.stateMap.error"
+ :svg-path="errorStateSvgPath"
+ :message="s__(`Pipelines|There was an error fetching the pipelines.
+ Try again in a few moments or contact your support team.`)"
/>
- <div
- class="blank-state-row"
- v-if="shouldRenderNoPipelinesMessage"
- >
- <div class="blank-state-center">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
- </div>
- </div>
+ <svg-blank-state
+ v-else-if="stateToRender === $options.stateMap.emptyTab"
+ :svg-path="noPipelinesSvgPath"
+ :message="emptyTabMessage"
+ />
<div
class="table-holder"
- v-if="shouldRenderTable"
+ v-else-if="stateToRender === $options.stateMap.tableList"
>
<pipelines-table-component
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 50bdf80c3e3..9fcc07abee5 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,23 +1,19 @@
import Visibility from 'visibilityjs';
+import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
-import emptyState from '../components/empty_state.vue';
-import errorState from '../components/error_state.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import pipelinesTableComponent from '../components/pipelines_table.vue';
+import EmptyState from '../components/empty_state.vue';
+import SvgBlankState from '../components/blank_state.vue';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
export default {
components: {
- pipelinesTableComponent,
- errorState,
- emptyState,
- loadingIcon,
- },
- computed: {
- shouldRenderErrorState() {
- return this.hasError && !this.isLoading;
- },
+ PipelinesTableComponent,
+ SvgBlankState,
+ EmptyState,
+ LoadingIcon,
},
data() {
return {
@@ -85,6 +81,7 @@ export default {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
+ this.hasMadeRequest = true;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
@@ -96,7 +93,7 @@ export default {
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines'))
- .catch(() => new Flash('An error occurred while making the request.'));
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
new file mode 100644
index 00000000000..3b17135f0e5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -0,0 +1,149 @@
+<script>
+import LabelsSelect from '~/labels_select';
+import LoadingIcon from '../../loading_icon.vue';
+
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
+import DropdownButton from './dropdown_button.vue';
+import DropdownHiddenInput from './dropdown_hidden_input.vue';
+import DropdownHeader from './dropdown_header.vue';
+import DropdownSearchInput from './dropdown_search_input.vue';
+import DropdownFooter from './dropdown_footer.vue';
+import DropdownCreateLabel from './dropdown_create_label.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ DropdownTitle,
+ DropdownValue,
+ DropdownValueCollapsed,
+ DropdownButton,
+ DropdownHiddenInput,
+ DropdownHeader,
+ DropdownSearchInput,
+ DropdownFooter,
+ DropdownCreateLabel,
+ },
+ props: {
+ showCreate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ abilityName: {
+ type: String,
+ required: true,
+ },
+ context: {
+ type: Object,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updatePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ labelsWebUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelFilterBasePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canEdit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hiddenInputName() {
+ return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
+ },
+ },
+ mounted() {
+ this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
+ handleClick: this.handleClick,
+ });
+ },
+ methods: {
+ handleClick(label) {
+ this.$emit('onLabelClick', label);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block labels">
+ <dropdown-value-collapsed
+ v-if="showCreate"
+ :labels="context.labels"
+ />
+ <dropdown-title
+ :can-edit="canEdit"
+ />
+ <dropdown-value
+ :labels="context.labels"
+ :label-filter-base-path="labelFilterBasePath"
+ >
+ <slot></slot>
+ </dropdown-value>
+ <div
+ v-if="canEdit"
+ class="selectbox"
+ style="display: none;"
+ >
+ <dropdown-hidden-input
+ v-for="label in context.labels"
+ :key="label.id"
+ :name="hiddenInputName"
+ :label="label"
+ />
+ <div class="dropdown">
+ <dropdown-button
+ :ability-name="abilityName"
+ :field-name="hiddenInputName"
+ :update-path="updatePath"
+ :labels-path="labelsPath"
+ :namespace="namespace"
+ :labels="context.labels"
+ :show-extra-options="!showCreate"
+ />
+ <div
+ class="dropdown-menu dropdown-select dropdown-menu-paging
+dropdown-menu-labels dropdown-menu-selectable"
+ >
+ <div class="dropdown-page-one">
+ <dropdown-header v-if="showCreate" />
+ <dropdown-search-input/>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ <dropdown-footer
+ v-if="showCreate"
+ :labels-web-url="labelsWebUrl"
+ />
+ </div>
+ <dropdown-create-label
+ v-if="showCreate"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
new file mode 100644
index 00000000000..47497c1de98
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -0,0 +1,78 @@
+<script>
+import { __, s__, sprintf } from '~/locale';
+
+export default {
+ props: {
+ abilityName: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ updatePath: {
+ type: String,
+ required: true,
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: true,
+ },
+ labels: {
+ type: Array,
+ required: true,
+ },
+ showExtraOptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownToggleText() {
+ if (this.labels.length === 0) {
+ return __('Label');
+ }
+
+ if (this.labels.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.labels[0].title,
+ remainingLabelCount: this.labels.length - 1,
+ });
+ }
+
+ return this.labels[0].title;
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ ref="dropdownButton"
+ class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
+ data-toggle="dropdown"
+ :class="{ 'js-extra-options': showExtraOptions }"
+ :data-ability-name="abilityName"
+ :data-field-name="fieldName"
+ :data-issue-update="updatePath"
+ :data-labels="labelsPath"
+ :data-namespace-path="namespace"
+ :data-show-any="showExtraOptions"
+ >
+ <span class="dropdown-toggle-text">
+ {{ dropdownToggleText }}
+ </span>
+ <i
+ aria-hidden="true"
+ class="fa fa-chevron-down"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
new file mode 100644
index 00000000000..4200d1e8473
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -0,0 +1,84 @@
+<script>
+export default {
+ created() {
+ this.suggestedColors = gon.suggested_label_colors;
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-page-two dropdown-new-label">
+ <div class="dropdown-title">
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-back"
+ :aria-label="__('Go back')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-arrow-left"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ {{ __('Create new label') }}
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+ <div class="dropdown-content">
+ <div class="dropdown-labels-error js-label-error"></div>
+ <input
+ id="new_label_name"
+ type="text"
+ class="default-dropdown-input"
+ :placeholder="__('Name new label')"
+ />
+ <div class="suggest-colors suggest-colors-dropdown">
+ <a
+ v-for="(color, index) in suggestedColors"
+ href="#"
+ :key="index"
+ :data-color="color"
+ :style="{
+ backgroundColor: color,
+ }"
+ >
+ &nbsp;
+ </a>
+ </div>
+ <div class="dropdown-label-color-input">
+ <div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
+ <input
+ id="new_label_color"
+ type="text"
+ class="default-dropdown-input"
+ :placeholder="__('Assign custom color like #FF0000')"
+ />
+ </div>
+ <div class="clearfix">
+ <button
+ type="button"
+ class="btn btn-primary pull-left js-new-label-btn disabled"
+ >
+ {{ __('Create') }}
+ </button>
+ <button
+ type="button"
+ class="btn btn-default pull-right js-cancel-label-btn"
+ >
+ {{ __('Cancel') }}
+ </button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
new file mode 100644
index 00000000000..e951a863811
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
@@ -0,0 +1,34 @@
+<script>
+export default {
+ props: {
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-footer">
+ <ul class="dropdown-footer-list">
+ <li>
+ <a
+ href="#"
+ class="dropdown-toggle-page"
+ >
+ {{ __('Create new label') }}
+ </a>
+ </li>
+ <li>
+ <a
+ data-is-link="true"
+ class="dropdown-external-link"
+ :href="labelsWebUrl"
+ >
+ {{ __('Manage labels') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
new file mode 100644
index 00000000000..7664acdf19c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -0,0 +1,21 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="dropdown-title">
+ <span>{{ __('Assign labels') }}</span>
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
new file mode 100644
index 00000000000..1832c3c1757
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
@@ -0,0 +1,22 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <input
+ type="hidden"
+ :name="name"
+ :value="label.id"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
new file mode 100644
index 00000000000..ae633460c95
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
@@ -0,0 +1,27 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="dropdown-input">
+ <input
+ autocomplete="off"
+ class="dropdown-input-field"
+ type="search"
+ :placeholder="__('Search')"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ data-hidden="true"
+ >
+ </i>
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
+ data-hidden="true"
+ role="button"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
new file mode 100644
index 00000000000..7da82e90e29
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="title hide-collapsed append-bottom-10">
+ {{ __('Labels') }}
+ <template v-if="canEdit">
+ <i
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin block-loading"
+ data-hidden="true"
+ >
+ </i>
+ <button
+ type="button"
+ class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
+ >
+ {{ __('Edit') }}
+ </button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
new file mode 100644
index 00000000000..ba4c8fba5ec
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -0,0 +1,63 @@
+<script>
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ labelFilterBasePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isEmpty() {
+ return this.labels.length === 0;
+ },
+ },
+ methods: {
+ labelFilterUrl(label) {
+ return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
+ },
+ labelStyle(label) {
+ return {
+ color: label.textColor,
+ backgroundColor: label.color,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="hide-collapsed value issuable-show-labels">
+ <span
+ v-if="isEmpty"
+ class="text-secondary"
+ >
+ <slot>{{ __('None') }}</slot>
+ </span>
+ <a
+ v-else
+ v-for="label in labels"
+ :key="label.id"
+ :href="labelFilterUrl(label)"
+ >
+ <span
+ v-tooltip
+ class="label color-label"
+ data-placement="bottom"
+ data-container="body"
+ :style="labelStyle(label)"
+ :title="label.description"
+ >
+ {{ label.title }}
+ </span>
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
new file mode 100644
index 00000000000..5cf728fe050
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -0,0 +1,48 @@
+<script>
+import { s__, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ labelsList() {
+ const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
+
+ if (this.labels.length > 5) {
+ return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
+ labelsString,
+ remainingLabelCount: this.labels.length - 5,
+ });
+ }
+
+ return labelsString;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-tooltip
+ class="sidebar-collapsed-icon"
+ data-placement="left"
+ data-container="body"
+ :title="labelsList"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-tags"
+ >
+ </i>
+ <span>{{ labels.length }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 98c1ec014c4..70b9efe0c68 100644
--- a/app/assets/javascripts/boards/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,7 +1,5 @@
-/* eslint-disable no-unused-vars, space-before-function-paren */
-
class ListLabel {
- constructor (obj) {
+ constructor(obj) {
this.id = obj.id;
this.title = obj.title;
this.type = obj.type;
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index 7a2c7234a1e..a7b562b1d8e 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -9,7 +9,6 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
- flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 52430ea771f..025d8270b7c 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -62,7 +62,7 @@ class InvitesController < ApplicationController
case source
when Project
project = member.source
- label = "project #{project.name_with_namespace}"
+ label = "project #{project.full_name}"
path = project_path(project)
when Group
group = member.source
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 74c25505e36..405726c017c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -38,7 +38,7 @@ class Projects::BlobController < Projects::ApplicationController
end
format.json do
- page_title @blob.path, @ref, @project.name_with_namespace
+ page_title @blob.path, @ref, @project.full_name
show_json
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index f752a46f828..ee9b5458282 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -36,7 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
end
format.json do
- page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
+ page_title @path.presence || _("Files"), @ref, @project.full_name
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 913689a1e74..ee197c75764 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -41,11 +41,11 @@ class ProjectsController < Projects::ApplicationController
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
redirect_to(
- project_path(@project),
+ project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
- render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
+ render 'new', locals: { active_tab: active_new_project_tab }
end
end
@@ -103,7 +103,7 @@ class ProjectsController < Projects::ApplicationController
def show
if @project.import_in_progress?
- redirect_to project_import_path(@project)
+ redirect_to project_import_path(@project, custom_import_params)
return
end
@@ -130,7 +130,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
- flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
redirect_to dashboard_projects_path, status: 302
rescue Projects::DestroyService::DestroyError => ex
@@ -359,6 +359,14 @@ class ProjectsController < Projects::ApplicationController
]
end
+ def custom_import_params
+ {}
+ end
+
+ def active_new_project_tab
+ project_params[:import_url].present? ? 'import' : 'blank'
+ end
+
def repo_exists?
project.repository_exists? && !project.empty_repo?
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a18ebfb6030..b484a868f92 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,19 +1,45 @@
module ImportHelper
+ def has_ci_cd_only_params?
+ false
+ end
+
def import_project_target(owner, name)
namespace = current_user.can_create_group? ? owner : current_user.namespace_path
"#{namespace}/#{name}"
end
- def provider_project_link(provider, path_with_namespace)
- url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend
+ def provider_project_link(provider, full_path)
+ url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend
+
+ link_to full_path, url, target: '_blank', rel: 'noopener noreferrer'
+ end
+
+ def import_will_timeout_message(_ci_cd_only)
+ timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)
+ _('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout }
+ end
+
+ def import_svn_message(_ci_cd_only)
+ svn_link = link_to _('this document'), help_page_path('user/project/import/svn')
+ _('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link }
+ end
+
+ def import_in_progress_title
+ if @project.forked?
+ _('Forking in progress')
+ else
+ _('Import in progress')
+ end
+ end
- link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
+ def import_wait_and_refresh_message
+ _('Please wait while we import the repository for you. Refresh at will.')
end
private
- def github_project_url(path_with_namespace)
- "#{github_root_url}/#{path_with_namespace}"
+ def github_project_url(full_path)
+ "#{github_root_url}/#{full_path}"
end
def github_root_url
@@ -23,7 +49,7 @@ module ImportHelper
@github_url = provider.fetch('url', 'https://github.com') if provider
end
- def gitea_project_url(path_with_namespace)
- "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
+ def gitea_project_url(full_path)
+ "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{full_path}"
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 44ecc2212f2..f6ddb6d4cfe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -99,7 +99,7 @@ module IssuablesHelper
project = Project.find_by(id: project_id)
if project
- project.name_with_namespace
+ project.full_name
else
default_label
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c1c19062c91..b2c641a5dbd 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -1,4 +1,5 @@
module LabelsHelper
+ extend self
include ActionView::Helpers::TagHelper
def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index cc1c69a1999..da9fe734f1c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -97,13 +97,13 @@ module ProjectsHelper
end
def remove_project_message(project)
- _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
- { project_name_with_namespace: project.name_with_namespace }
+ _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
+ { project_full_name: project.full_name }
end
def transfer_project_message(project)
- _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") %
- { project_name_with_namespace: project.name_with_namespace }
+ _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
+ { project_full_name: project.full_name }
end
def remove_fork_project_message(project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index e6a6496871a..761c1252fc8 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -110,7 +110,7 @@ module SearchHelper
category: "Projects",
id: p.id,
value: "#{search_result_sanitize(p.name)}",
- label: "#{search_result_sanitize(p.name_with_namespace)}",
+ label: "#{search_result_sanitize(p.full_name)}",
url: project_path(p)
}
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index ddb48371c79..f7620e0b6b8 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -114,7 +114,7 @@ module TodosHelper
projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.map do |project|
- { id: project.id, text: project.name_with_namespace }
+ { id: project.id, text: project.full_name }
end
projects.unshift({ id: '', text: 'Any Project' }).to_json
diff --git a/app/models/badge.rb b/app/models/badge.rb
new file mode 100644
index 00000000000..f7e10c2ebfc
--- /dev/null
+++ b/app/models/badge.rb
@@ -0,0 +1,51 @@
+class Badge < ActiveRecord::Base
+ # This structure sets the placeholders that the urls
+ # can have. This hash also sets which action to ask when
+ # the placeholder is found.
+ PLACEHOLDERS = {
+ 'project_path' => :full_path,
+ 'project_id' => :id,
+ 'default_branch' => :default_branch,
+ 'commit_sha' => ->(project) { project.commit&.sha }
+ }.freeze
+
+ # This regex is built dynamically using the keys from the PLACEHOLDER struct.
+ # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
+ # This regex will build the new PLACEHOLDER_REGEX with the new information
+ PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze
+
+ default_scope { order_created_at_asc }
+
+ scope :order_created_at_asc, -> { reorder(created_at: :asc) }
+
+ validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX }
+ validates :type, presence: true
+
+ def rendered_link_url(project = nil)
+ build_rendered_url(link_url, project)
+ end
+
+ def rendered_image_url(project = nil)
+ build_rendered_url(image_url, project)
+ end
+
+ private
+
+ def build_rendered_url(url, project = nil)
+ return url unless valid? && project
+
+ Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg|
+ replace_placeholder_action(PLACEHOLDERS[arg], project)
+ end
+ end
+
+ # The action param represents the :symbol or Proc to call in order
+ # to retrieve the return value from the project.
+ # This method checks if it is a Proc and use the call method, and if it is
+ # a symbol just send the action
+ def replace_placeholder_action(action, project)
+ return unless project
+
+ action.is_a?(Proc) ? action.call(project) : project.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb
new file mode 100644
index 00000000000..f4b2bdecdcc
--- /dev/null
+++ b/app/models/badges/group_badge.rb
@@ -0,0 +1,5 @@
+class GroupBadge < Badge
+ belongs_to :group
+
+ validates :group, presence: true
+end
diff --git a/app/models/badges/project_badge.rb b/app/models/badges/project_badge.rb
new file mode 100644
index 00000000000..3945b376052
--- /dev/null
+++ b/app/models/badges/project_badge.rb
@@ -0,0 +1,15 @@
+class ProjectBadge < Badge
+ belongs_to :project
+
+ validates :project, presence: true
+
+ def rendered_link_url(project = nil)
+ project ||= self.project
+ super
+ end
+
+ def rendered_image_url(project = nil)
+ project ||= self.project
+ super
+ end
+end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 7adf1663c35..16efe90fa27 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -56,12 +56,13 @@ module Clusters
def specification
{
"gitlabUrl" => gitlab_url,
- "runnerToken" => ensure_runner.token
+ "runnerToken" => ensure_runner.token,
+ "runners" => { "privileged" => privileged }
}
end
def content_values
- specification.merge(YAML.load_file(chart_values_file))
+ YAML.load_file(chart_values_file).deep_merge!(specification)
end
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 75538ba196c..be0fc7efa9a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -158,7 +158,7 @@ class Event < ActiveRecord::Base
def project_name
if project
- project.name_with_namespace
+ project.full_name
else
"(deleted project)"
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 75bf013ecd2..201505c3d3c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -31,6 +31,8 @@ class Group < Namespace
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :badges, class_name: 'GroupBadge'
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c1c27ccf3e5..06aa67c600f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
- def commits_count
- super || merge_request_diff_commits.size
- end
-
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/project.rb b/app/models/project.rb
index 5b1f8b2658b..a11b1e4f554 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -221,6 +221,8 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
+ has_many :project_badges, class_name: 'ProjectBadge'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
@@ -1766,6 +1768,17 @@ class Project < ActiveRecord::Base
.set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
end
+ def badges
+ return project_badges unless group
+
+ group_badges_rel = GroupBadge.where(group: group.self_and_ancestors)
+
+ union = Gitlab::SQL::Union.new([project_badges.select(:id),
+ group_badges_rel.select(:id)])
+
+ Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ end
+
private
def storage
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 109258d1eb7..4f289e6e215 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -68,7 +68,7 @@ http://app.asana.com/-/account_api'
end
user = data[:user_name]
- project_name = project.name_with_namespace
+ project_name = project.full_name
data[:commits].each do |commit|
push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index c3f5b310619..8d7a4fceb08 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -86,7 +86,7 @@ class CampfireService < Service
after = push[:after]
message = ""
- message << "[#{project.name_with_namespace}] "
+ message << "[#{project.full_name}] "
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index e2ffdf72201..dab0ea1a681 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -129,7 +129,7 @@ class ChatNotificationService < Service
end
def project_name
- project.name_with_namespace.gsub(/\s/, '')
+ project.full_name.gsub(/\s/, '')
end
def project_url
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index bfe7ac29c18..f31c3f02af2 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -120,7 +120,7 @@ class HipchatService < Service
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.name_with_namespace.gsub!(/\s/, '')}</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|
@@ -274,7 +274,7 @@ class HipchatService < Service
end
def project_name
- project.name_with_namespace.gsub(/\s/, '')
+ project.full_name.gsub(/\s/, '')
end
def project_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 436a870b0c4..e5035c81df0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,5 +1,7 @@
class JiraService < IssueTrackerService
include Gitlab::Routing
+ include ApplicationHelper
+ include ActionView::Helpers::AssetUrlHelper
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
@@ -268,7 +270,9 @@ class JiraService < IssueTrackerService
url: url,
title: title,
status: status,
- icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ icon: {
+ title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
+ }
}
}
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 4d2037286a2..227d430083d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -37,7 +37,7 @@ class MattermostSlashCommandsService < SlashCommandsService
private
def command(params)
- pretty_project_name = project.name_with_namespace
+ pretty_project_name = project.full_name
params.merge(
auto_complete: true,
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index aa7bd4c3c84..e3a1ca2d45f 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -88,10 +88,10 @@ class PushoverService < Service
user: user_key,
device: device,
priority: priority,
- title: "#{project.name_with_namespace}",
+ title: "#{project.full_name}",
message: message,
url: data[:project][:web_url],
- url_title: "See project #{project.name_with_namespace}"
+ url_title: "See project #{project.full_name}"
}
# Sound parameter MUST NOT be sent to API if not selected
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 242d9d5f125..1a14afb951a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -253,7 +253,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
- return unless sha && commit_by(oid: sha)
+ return unless sha.present? && commit_by(oid: sha)
return if kept_around?(sha)
diff --git a/app/services/badges/base_service.rb b/app/services/badges/base_service.rb
new file mode 100644
index 00000000000..4f87426bd38
--- /dev/null
+++ b/app/services/badges/base_service.rb
@@ -0,0 +1,11 @@
+module Badges
+ class BaseService
+ protected
+
+ attr_accessor :params
+
+ def initialize(params = {})
+ @params = params.dup
+ end
+ end
+end
diff --git a/app/services/badges/build_service.rb b/app/services/badges/build_service.rb
new file mode 100644
index 00000000000..6267e571838
--- /dev/null
+++ b/app/services/badges/build_service.rb
@@ -0,0 +1,12 @@
+module Badges
+ class BuildService < Badges::BaseService
+ # returns the created badge
+ def execute(source)
+ if source.is_a?(Group)
+ GroupBadge.new(params.merge(group: source))
+ else
+ ProjectBadge.new(params.merge(project: source))
+ end
+ end
+ end
+end
diff --git a/app/services/badges/create_service.rb b/app/services/badges/create_service.rb
new file mode 100644
index 00000000000..aafb87f7dcd
--- /dev/null
+++ b/app/services/badges/create_service.rb
@@ -0,0 +1,10 @@
+module Badges
+ class CreateService < Badges::BaseService
+ # returns the created badge
+ def execute(source)
+ badge = Badges::BuildService.new(params).execute(source)
+
+ badge.tap { |b| b.save }
+ end
+ end
+end
diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb
new file mode 100644
index 00000000000..7ca84b5df31
--- /dev/null
+++ b/app/services/badges/update_service.rb
@@ -0,0 +1,12 @@
+module Badges
+ class UpdateService < Badges::BaseService
+ # returns the updated badge
+ def execute(badge)
+ if params.present?
+ badge.update_attributes(params)
+ end
+
+ badge
+ end
+ end
+end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index af8c02a10b7..ba7946fd23c 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -20,8 +20,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema,
- updated_at: model.updated_at.xmlschema
+ created_at: model.created_at&.xmlschema,
+ updated_at: model.updated_at&.xmlschema
}
case model
diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb
new file mode 100644
index 00000000000..dd681218b6b
--- /dev/null
+++ b/app/validators/url_placeholder_validator.rb
@@ -0,0 +1,32 @@
+# UrlValidator
+#
+# Custom validator for URLs.
+#
+# By default, only URLs for the HTTP(S) protocols will be considered valid.
+# Provide a `:protocols` option to configure accepted protocols.
+#
+# Also, this validator can help you validate urls with placeholders inside.
+# Usually, if you have a url like 'http://www.example.com/%{project_path}' the
+# URI parser will reject that URL format. Provide a `:placeholder_regex` option
+# to configure accepted placeholders.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, url: true
+#
+# validates :ftp_url, url: { protocols: %w(ftp) }
+#
+# validates :git_url, url: { protocols: %w(http https ssh git) }
+#
+# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ }
+# end
+#
+class UrlPlaceholderValidator < UrlValidator
+ def validate_each(record, attribute, value)
+ placeholder_regex = self.options[:placeholder_regex]
+ value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value
+
+ super(record, attribute, value)
+ end
+end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e3711421b61..05c41082882 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -164,7 +164,7 @@
%h4 Latest projects
- @projects.each do |project|
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
%span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 2545cecc721..324f3c0a22f 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -68,7 +68,7 @@
- @projects.each do |project|
%li
%strong
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
@@ -86,7 +86,7 @@
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 42f92079d85..c02ddafe108 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,8 +1,8 @@
- add_to_breadcrumbs "Projects", admin_projects_path
-- breadcrumb_title @project.name_with_namespace
-- page_title @project.name_with_namespace, "Projects"
+- breadcrumb_title @project.full_name
+- page_title @project.full_name, "Projects"
%h3.page-title
- Project: #{@project.name_with_namespace}
+ Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 6d8fad0eb8d..185e9d7b35d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -39,7 +39,7 @@
%tr.alert-info
%td
%strong
- = project.name_with_namespace
+ = project.full_name
%td
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
@@ -61,7 +61,7 @@
- @projects.each do |project|
%tr
%td
- = project.name_with_namespace
+ = project.full_name
%td
.pull-right
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
@@ -95,7 +95,7 @@
%td.status
- if project
- = project.name_with_namespace
+ = project.full_name
%td.build-link
- if project
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 4a440f3f6d4..96835ee9af5 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -29,12 +29,12 @@
.panel.panel-default
.panel-heading Joined projects (#{@joined_projects.count})
%ul.well-list
- - @joined_projects.sort_by(&:name_with_namespace).each do |project|
+ - @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
.list-item-name
= link_to admin_project_path(project), class: dom_class(project) do
- = project.name_with_namespace
+ = project.full_name
- if member
.pull-right
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 8d2bc810a7d..ef181b425bc 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -14,7 +14,7 @@
.list-item-name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
- %strong= link_to project.name_with_namespace, project
+ %strong= link_to project.full_name, project
.pull-right
- if project.archived
%span.label.label-warning archived
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index ad6213b4efd..c2bb1216c5f 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -12,7 +12,7 @@
- project = @member.source
project
%strong
- = link_to project.name_with_namespace, project_url(project)
+ = link_to project.full_name, project_url(project)
- when Group
- group = @member.source
group
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index 59becb043d3..5809d6f7fea 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,4 +1,4 @@
-- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
+- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 6b847fb4b7c..6b51483810e 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,4 +1,4 @@
-- page_title @project.name_with_namespace
+- page_title @project.full_name
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
- nav "project"
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index f0ba7827cef..71c62f6be4e 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -3,6 +3,6 @@
%p
The project export can be downloaded from:
= link_to download_export_project_url(@project), rel: 'nofollow', download: '' do
- = @project.name_with_namespace + " export"
+ = @project.full_name + " export"
%p
The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index c476a39b661..1b6b1a81665 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -3,7 +3,7 @@
%p
The project is now located under
= link_to project_url(@project) do
- = @project.name_with_namespace
+ = @project.full_name
%p
To update the remote url in your local repository run (for ssh):
%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index fe1cf802971..c7094800fb2 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -4,7 +4,7 @@
%td
%strong
- if can?(current_user, :read_project, project)
- = link_to project.name_with_namespace, project_path(project)
+ = link_to project.full_name, project_path(project)
- else
.light N/A
%td
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 457583cfd35..1e206def7ee 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -12,7 +12,9 @@
Add an SSH key
%p.profile-settings-content
Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh/README")
+ = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
+ or use an
+ = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
= render 'form'
%hr
%h5
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b565f14747a..a2ecfddb163 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -23,6 +23,12 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
+ .project-badges
+ - @project.badges.each do |badge|
+ - badge_link_url = badge.rendered_link_url(@project)
+ %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' }
+ %img{ src: badge.rendered_image_url(@project), alt: badge_link_url }
+
.project-repo-buttons
.count-buttons
= render 'projects/buttons/star'
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index d367bd6be7b..f4b5ef1555e 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -1,6 +1,8 @@
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
+- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
.row{ id: project_name_id }
+ = f.hidden_field :ci_cd_only, value: ci_cd_only
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-light' do
%span
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 0cd2d45c74b..9126476e79e 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
- if admin
%td
- if job.project
- = link_to job.project.name_with_namespace, admin_project_path(job.project)
+ = link_to job.project.full_name, admin_project_path(job.project)
%td
- if job.try(:runner)
= runner_link(job.runner)
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 2599ce5c4b8..620fd1906ba 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -53,7 +53,7 @@
- if admin
%td
- if generic_commit_status.project
- = link_to generic_commit_status.project.name_with_namespace, admin_project_path(generic_commit_status.project)
+ = link_to generic_commit_status.project.full_name, admin_project_path(generic_commit_status.project)
%td
- if generic_commit_status.try(:runner)
= runner_link(generic_commit_status.runner)
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 8c490773a56..3b0c828ccd1 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,12 +1,11 @@
-- page_title @project.forked? ? "Forking in progress" : "Import in progress"
+- page_title import_in_progress_title
+
.save-project-loader
.center
%h2
%i.fa.fa-spinner.fa-spin
- - if @project.forked?
- Forking in progress.
- - else
- Import in progress.
- - if @project.external_import?
+ = import_in_progress_title
+ - if !has_ci_cd_only_params? && @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
- %p Please wait while we import the repository for you. Refresh at will.
+ %p
+ = import_wait_and_refresh_message
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 5f97d31f610..5c36d2202a6 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -18,7 +18,7 @@
- unless @issue.project.id == merge_request.target_project.id
in
- project = merge_request.target_project
- = link_to project.name_with_namespace, project_path(project)
+ = link_to project.full_name, project_path(project)
- if merge_request.merged?
%span.merge-request-status.prepend-left-10.merged
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 679ba23a4db..1d31b58a2cc 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -12,11 +12,14 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- New project
+ = _('New project')
%p
- A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'
+ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
- All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
+ = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
+ -# EE-specific start
+ -# EE-specific end
.md
= brand_new_project_guidelines
%p
@@ -28,36 +31,38 @@
.col-lg-9.js-toggle-container
%ul.nav-links.gitlab-tabs{ role: 'tablist' }
- %li{ class: ('active' if active_tab == 'blank'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'blank'), role: 'presentation' }
%a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Blank project
%span.visible-xs Blank
- %li{ class: ('active' if active_tab == 'template'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'template'), role: 'presentation' }
%a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Create from template
%span.visible-xs Template
- %li{ class: ('active' if active_tab == 'import'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'import'), role: 'presentation' }
%a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Import project
%span.visible-xs Import
+ -# EE-specific start
+ -# EE-specific end
.tab-content.gitlab-tab-content
- .tab-pane{ id: 'blank-project-pane', class: ('active' if active_tab == 'blank'), role: 'tabpanel' }
+ .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
- .tab-pane.no-padding{ id: 'create-from-template-pane', class: ('active' if active_tab == 'template'), role: 'tabpanel' }
+ .tab-pane.no-padding{ id: 'create-from-template-pane', class: active_when(active_tab == 'template'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
= render 'project_templates', f: f
- .tab-pane.import-project-pane{ id: 'import-project-pane', class: ('active' if active_tab == 'import'), role: 'tabpanel' }
+ .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
.project-import.row
- .col-sm-12
+ .col-lg-12
.form-group.import-btn-container.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from
@@ -97,7 +102,7 @@
Gitea
%div
- if git_import_enabled?
- %button.btn.js-toggle-button.import_git{ type: "button" }
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
@@ -105,6 +110,10 @@
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
+
+ -# EE-specific start
+ -# EE-specific end
+
.save-project-loader.hide
.center
%h2
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index cf95cdbfec2..3e6b3346787 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -7,8 +7,9 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
- "new-pipeline-path" => new_project_pipeline_path(@project),
+ "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
- "has-ci" => @repository.gitlab_ci_yml,
- "ci-lint-path" => ci_lint_path,
- "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
+ "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
+ "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
+ "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
+ "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 5dbcbf7eba6..2ab0227126a 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,4 +1,4 @@
-- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}"
%p To setup this service:
%ul.list-unstyled.indent-list
@@ -20,7 +20,7 @@
.form-group
= label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(target: '#display_name')
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index c31c95608c6..d592a5e4663 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,4 +1,4 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index e43796e9654..e4902d368e7 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -22,7 +22,7 @@
%span.dropdown-toggle-text
Project:
- if @project.present?
- = @project.name_with_namespace
+ = @project.full_name
- else
Any
= icon("chevron-down")
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 60ef44482f0..ab56f48ba4d 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -6,7 +6,7 @@
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
- in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
+ in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]}
- elsif @group
in group #{link_to @group.name, @group}
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index b4bc8982c05..b7a27ef6be2 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -10,4 +10,4 @@
.description.term
= search_md_sanitize(issue, :description)
%span.light
- #{issue.project.name_with_namespace}
+ #{issue.project.full_name}
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 1a5499e4d58..8b0fd74f680 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -11,4 +11,4 @@
.description.term
= search_md_sanitize(merge_request, :description)
%span.light
- #{merge_request.project.name_with_namespace}
+ #{merge_request.project.full_name}
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index a7e178dfa71..e4ab7b0541f 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -7,7 +7,7 @@
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
commented on
- = link_to project.name_with_namespace, project
+ = link_to project.full_name, project
&middot;
- if note.for_commit?
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 65710c09a89..d46c4d11e51 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -11,7 +11,7 @@
%small.pull-right.cgray
- if snippet_title.project_id?
- = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project)
+ = link_to snippet_title.project.full_name, project_path(snippet_title.project)
.snippet-info
= snippet_title.to_reference
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index de52fd00157..7d3e243495f 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,7 +1,7 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
-- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
+- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
Unsubscribe from #{noteable_type}
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 736afa085e8..5eaaa1448d5 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,17 +1,22 @@
+- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+
.form-group.import-url-data
= f.label :import_url, class: 'label-light' do
- %span Git repository URL
+ %span
+ = _('Git repository URL')
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
.well.prepend-top-20
%ul
%li
- The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
+ = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
%li
- If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe
%li
- The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}.
- For repositories that take longer, use a clone/push combination.
+ = import_will_timeout_message(ci_cd_only)
%li
- To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
+ = import_svn_message(ci_cd_only)
+
+-# EE-specific start
+-# EE-specific end
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 129f6ab604e..eba64daaadc 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -12,7 +12,7 @@
- if show_project_name
%strong #{project.name} &middot;
- elsif show_full_project_name
- %strong #{project.name_with_namespace} &middot;
+ %strong #{project.full_name} &middot;
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
= link_to issuable.title, issuable_url_args, title: issuable.title
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index e3b2b53833e..da01fc02d07 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -27,7 +27,7 @@
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
- = dashboard ? milestone.project.name_with_namespace : milestone.project.name
+ = dashboard ? milestone.project.full_name : milestone.project.name
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index fd0760d83a5..6006ab8b43f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -56,7 +56,7 @@
- milestone.milestones.each do |ms|
%tr
%td
- - project_name = group ? ms.project.name : ms.project.name_with_namespace
+ - project_name = group ? ms.project.name : ms.project.full_name
= link_to project_name, project_milestone_path(ms.project, ms)
%td
= ms.issues_visible_to_user(current_user).opened.count
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 491a8a41090..3acec88c2e3 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -31,7 +31,7 @@
%span.hidden-xs
in
= link_to project_path(snippet.project) do
- = snippet.project.name_with_namespace
+ = snippet.project.full_name
.pull-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 9a9fbaad653..100d86e38c8 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -22,7 +22,7 @@ module Gitlab
importer_class.new(object, project, client).execute
- counter.increment(project: project.path_with_namespace)
+ counter.increment(project: project.full_path)
end
def counter
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 7ba224d74c8..55fb817ca6e 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -44,6 +44,10 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
+
+ # In case pack files are deleted, release libgit2 cache and open file
+ # descriptors ASAP instead of waiting for Ruby garbage collection
+ project.cleanup
ensure
cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
end
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index 073d6608082..a779e631516 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -16,7 +16,7 @@ module Gitlab
def report_import_time(project)
duration = Time.zone.now - project.created_at
- path = project.path_with_namespace
+ path = project.full_path
histogram.observe({ project: path }, duration)
counter.increment
diff --git a/changelogs/unreleased/38587-pipelines-empty-state.yml b/changelogs/unreleased/38587-pipelines-empty-state.yml
new file mode 100644
index 00000000000..58ea204d394
--- /dev/null
+++ b/changelogs/unreleased/38587-pipelines-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Handle empty state in Pipelines page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml b/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml
new file mode 100644
index 00000000000..08109632e8e
--- /dev/null
+++ b/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml
@@ -0,0 +1,5 @@
+---
+title: Enable privileged mode for GitLab Runner
+merge_request: 17528
+author:
+type: added
diff --git a/changelogs/unreleased/43829-update-ssh-addtion-text.yml b/changelogs/unreleased/43829-update-ssh-addtion-text.yml
new file mode 100644
index 00000000000..b7052bb171e
--- /dev/null
+++ b/changelogs/unreleased/43829-update-ssh-addtion-text.yml
@@ -0,0 +1,5 @@
+---
+title: Update SSH key link to include existing keys
+merge_request:
+author: Brendan O'Leary
+type: changed
diff --git a/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml b/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml
new file mode 100644
index 00000000000..23a870d6e9f
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Store sha256 checksum to job artifacts
+merge_request: 17354
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml b/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml
new file mode 100644
index 00000000000..7cb12e26332
--- /dev/null
+++ b/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml
@@ -0,0 +1,5 @@
+---
+title: Implemented badge API endpoints
+merge_request: 17082
+author:
+type: added
diff --git a/changelogs/unreleased/issue_31081.yml b/changelogs/unreleased/issue_31081.yml
new file mode 100644
index 00000000000..ac547c285db
--- /dev/null
+++ b/changelogs/unreleased/issue_31081.yml
@@ -0,0 +1,5 @@
+---
+title: Use host URL to build JIRA remote link icon
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/kp-label-select-vue.yml b/changelogs/unreleased/kp-label-select-vue.yml
new file mode 100644
index 00000000000..1f5952f2554
--- /dev/null
+++ b/changelogs/unreleased/kp-label-select-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Port Labels Select dropdown to Vue
+merge_request: 17411
+author:
+type: other
diff --git a/changelogs/unreleased/oauth_generic_provider.yml b/changelogs/unreleased/oauth_generic_provider.yml
new file mode 100644
index 00000000000..3b6f8b04529
--- /dev/null
+++ b/changelogs/unreleased/oauth_generic_provider.yml
@@ -0,0 +1,4 @@
+---
+title: Make oauth provider login generic
+merge_request: 8809
+author: Horatiu Eugen Vlad \ No newline at end of file
diff --git a/changelogs/unreleased/sh-cleanup-after-git-gc.yml b/changelogs/unreleased/sh-cleanup-after-git-gc.yml
new file mode 100644
index 00000000000..4b652f4d6ce
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-after-git-gc.yml
@@ -0,0 +1,5 @@
+---
+title: Release libgit2 cache and open file descriptors after `git gc` run
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml b/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml
new file mode 100644
index 00000000000..7c7ef39cb75
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml
@@ -0,0 +1,5 @@
+---
+title: Don't error out in system hook if user has `nil` datetime columns
+merge_request:
+author:
+type: fixed
diff --git a/config.ru b/config.ru
index de0400f4f67..7b15939c6ff 100644
--- a/config.ru
+++ b/config.ru
@@ -23,5 +23,6 @@ warmup do |app|
end
map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do
+ use Gitlab::Middleware::ReleaseEnv
run Gitlab::Application
end
diff --git a/config/application.rb b/config/application.rb
index 918bd4d57cf..74fe3e439ed 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -26,6 +26,7 @@ module Gitlab
# This is a nice reference article on autoloading/eager loading:
# http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload
config.eager_load_paths.push(*%W[#{config.root}/lib
+ #{config.root}/app/models/badges
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
diff --git a/db/migrate/20180214093516_create_badges.rb b/db/migrate/20180214093516_create_badges.rb
new file mode 100644
index 00000000000..6559f834484
--- /dev/null
+++ b/db/migrate/20180214093516_create_badges.rb
@@ -0,0 +1,17 @@
+class CreateBadges < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :badges do |t|
+ t.string :link_url, null: false
+ t.string :image_url, null: false
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: true
+ t.integer :group_id, index: true, null: true
+ t.string :type, null: false
+
+ t.timestamps_with_timezone null: false
+ end
+
+ add_foreign_key :badges, :namespaces, column: :group_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb b/db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb
new file mode 100644
index 00000000000..54e6e35449e
--- /dev/null
+++ b/db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb
@@ -0,0 +1,7 @@
+class AddChecksumToCiJobArtifacts < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :ci_job_artifacts, :file_sha256, :binary
+ end
+end
diff --git a/db/migrate/20180304204842_clean_commits_count_migration.rb b/db/migrate/20180304204842_clean_commits_count_migration.rb
new file mode 100644
index 00000000000..ace4c6aa1cf
--- /dev/null
+++ b/db/migrate/20180304204842_clean_commits_count_migration.rb
@@ -0,0 +1,14 @@
+class CleanCommitsCountMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('AddMergeRequestDiffCommitsCount')
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20180305144721_add_privileged_to_runner.rb b/db/migrate/20180305144721_add_privileged_to_runner.rb
new file mode 100644
index 00000000000..32e73dba8d5
--- /dev/null
+++ b/db/migrate/20180305144721_add_privileged_to_runner.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPrivilegedToRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :clusters_applications_runners, :privileged, :boolean, default: true, allow_null: false
+ end
+
+ def down
+ remove_column :clusters_applications_runners, :privileged
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index db8bafe9677..0881a1af945 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180301084653) do
+ActiveRecord::Schema.define(version: 20180305144721) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -183,6 +183,19 @@ ActiveRecord::Schema.define(version: 20180301084653) do
add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
+ create_table "badges", force: :cascade do |t|
+ t.string "link_url", null: false
+ t.string "image_url", null: false
+ t.integer "project_id"
+ t.integer "group_id"
+ t.string "type", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "badges", ["group_id"], name: "index_badges_on_group_id", using: :btree
+ add_index "badges", ["project_id"], name: "index_badges_on_project_id", using: :btree
+
create_table "boards", force: :cascade do |t|
t.integer "project_id", null: false
t.datetime "created_at", null: false
@@ -333,6 +346,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do
t.datetime_with_timezone "updated_at", null: false
t.datetime_with_timezone "expire_at"
t.string "file"
+ t.binary "file_sha256"
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
@@ -590,6 +604,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do
t.datetime_with_timezone "updated_at", null: false
t.string "version", null: false
t.text "status_reason"
+ t.boolean "privileged", default: true, null: false
end
add_index "clusters_applications_runners", ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree
@@ -1969,6 +1984,8 @@ ActiveRecord::Schema.define(version: 20180301084653) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
+ add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "badges", "projects", on_delete: :cascade
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
diff --git a/doc/api/README.md b/doc/api/README.md
index b193ef4ab7f..53f1a70c1aa 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -24,6 +24,7 @@ following locations:
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
+- [Group Badges](group_badges.md)
- [Group Members](members.md)
- [Issues](issues.md)
- [Issue Boards](boards.md)
@@ -43,6 +44,7 @@ following locations:
- [Pipeline Schedules](pipeline_schedules.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
+- [Project Badges](project_badges.md)
- [Project import/export](project_import_export.md)
- [Project Members](members.md)
- [Project Snippets](project_snippets.md)
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
new file mode 100644
index 00000000000..3e0683f378d
--- /dev/null
+++ b/doc/api/group_badges.md
@@ -0,0 +1,191 @@
+# Group badges API
+
+## Placeholder tokens
+
+Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are:
+
+- **%{project_path}**: will be replaced by the project path.
+- **%{project_id}**: will be replaced by the project id.
+- **%{default_branch}**: will be replaced by the project default branch.
+- **%{commit_sha}**: will be replaced by the last project's commit sha.
+
+Because these enpoints aren't inside a project's context, the information used to replace the placeholders will be
+from the first group's project by creation date. If the group hasn't got any project the original URL with the placeholders will be returned.
+
+## List all badges of a group
+
+Gets a list of a group's badges.
+
+```
+GET /groups/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+ },
+ {
+ "id": 2,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+ },
+]
+```
+
+## Get a badge of a group
+
+Gets a badge of a group.
+
+```
+GET /groups/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+}
+```
+
+## Add a badge to a group
+
+Adds a badge to a group.
+
+```
+POST /groups/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link |
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "link_url=https://gitlab.com/gitlab-org/gitlab-ce/commits/master&image_url=https://shields.io/my/badge1&position=0" https://gitlab.example.com/api/v4/groups/:id/badges
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge1",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge1",
+ "kind": "group"
+}
+```
+
+## Edit a badge of a group
+
+Updates a badge of a group.
+
+```
+PUT /groups/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+| `link_url` | string | no | URL of the badge link |
+| `image_url` | string | no | URL of the badge image |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+}
+```
+
+## Remove a badge from a group
+
+Removes a badge from a group.
+
+```
+DELETE /groups/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/:badge_id
+```
+
+## Preview a badge from a group
+
+Returns how the `link_url` and `image_url` final URLs would be after resolving the placeholder interpolation.
+
+```
+GET /groups/:id/badges/render
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link|
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/render?link_url=http%3A%2F%2Fexample.com%2Fci_status.svg%3Fproject%3D%25%7Bproject_path%7D%26ref%3D%25%7Bdefault_branch%7D&image_url=https%3A%2F%2Fshields.io%2Fmy%2Fbadge
+```
+
+Example response:
+
+```json
+{
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+}
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index f50558b58a6..1aed8aac64e 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -525,3 +525,7 @@ And to switch pages add:
```
[ce-15142]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15142
+
+## Group badges
+
+Read more in the [Group Badges](group_badges.md) documentation.
diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md
new file mode 100644
index 00000000000..3f6e348b5b4
--- /dev/null
+++ b/doc/api/project_badges.md
@@ -0,0 +1,188 @@
+# Project badges API
+
+## Placeholder tokens
+
+Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are:
+
+- **%{project_path}**: will be replaced by the project path.
+- **%{project_id}**: will be replaced by the project id.
+- **%{default_branch}**: will be replaced by the project default branch.
+- **%{commit_sha}**: will be replaced by the last project's commit sha.
+
+## List all badges of a project
+
+Gets a list of a project's badges and its group badges.
+
+```
+GET /projects/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "project"
+ },
+ {
+ "id": 2,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+ },
+]
+```
+
+## Get a badge of a project
+
+Gets a badge of a project.
+
+```
+GET /projects/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "project"
+}
+```
+
+## Add a badge to a project
+
+Adds a badge to a project.
+
+```
+POST /projects/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project ](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link |
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "link_url=https://gitlab.com/gitlab-org/gitlab-ce/commits/master&image_url=https://shields.io/my/badge1&position=0" https://gitlab.example.com/api/v4/projects/:id/badges
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge1",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge1",
+ "kind": "project"
+}
+```
+
+## Edit a badge of a project
+
+Updates a badge of a project.
+
+```
+PUT /projects/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+| `link_url` | string | no | URL of the badge link |
+| `image_url` | string | no | URL of the badge image |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "project"
+}
+```
+
+## Remove a badge from a project
+
+Removes a badge from a project. Only project's badges will be removed by using this endpoint.
+
+```
+DELETE /projects/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/:badge_id
+```
+
+## Preview a badge from a project
+
+Returns how the `link_url` and `image_url` final URLs would be after resolving the placeholder interpolation.
+
+```
+GET /projects/:id/badges/render
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link|
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/render?link_url=http%3A%2F%2Fexample.com%2Fci_status.svg%3Fproject%3D%25%7Bproject_path%7D%26ref%3D%25%7Bdefault_branch%7D&image_url=https%3A%2F%2Fshields.io%2Fmy%2Fbadge
+```
+
+Example response:
+
+```json
+{
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+}
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b6442cfac22..271ee91dc72 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1340,3 +1340,7 @@ Read more in the [Project import/export](project_import_export.md) documentation
## Project members
Read more in the [Project members](members.md) documentation.
+
+## Project badges
+
+Read more in the [Project Badges](project_badges.md) documentation.
diff --git a/doc/user/project/import/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png
new file mode 100644
index 00000000000..ec867da1087
--- /dev/null
+++ b/doc/user/project/import/img/import_projects_from_repo_url.png
Binary files differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index e2b285678c3..72cc58546b7 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -10,6 +10,7 @@
1. [From Perforce](perforce.md)
1. [From SVN](svn.md)
1. [From TFS](tfs.md)
+1. [From repo by URL](repo_by_url.md)
In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
diff --git a/doc/user/project/import/repo_by_url.md b/doc/user/project/import/repo_by_url.md
new file mode 100644
index 00000000000..f43e384de88
--- /dev/null
+++ b/doc/user/project/import/repo_by_url.md
@@ -0,0 +1,12 @@
+# Import project from repo by URL
+
+You can import your existing repositories by providing the Git URL:
+
+1. From your GitLab dashboard click **New project**
+1. Switch to the **Import project** tab
+1. Click on the **Repo by URL** button
+1. Fill in the "Git repository URL" and the remaining project fields
+1. Click **Create project** to being the import process
+1. Once complete, you will be redirected to your newly created project
+
+![Import project by repo URL](img/import_projects_from_repo_url.png)
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index dedf102fc37..5ddeb014b30 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -31,7 +31,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.4 to current | 0.2.2 |
+| 10.6 to current | 0.2.3 |
+| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 754549f72f0..b1b247b70b9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -108,6 +108,7 @@ module API
mount ::API::AccessRequests
mount ::API::Applications
mount ::API::AwardEmoji
+ mount ::API::Badges
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
new file mode 100644
index 00000000000..334948b2995
--- /dev/null
+++ b/lib/api/badges.rb
@@ -0,0 +1,134 @@
+module API
+ class Badges < Grape::API
+ include PaginationParams
+
+ before { authenticate_non_get! }
+
+ helpers ::API::Helpers::BadgesHelpers
+
+ helpers do
+ def find_source_if_admin(source_type)
+ source = find_source(source_type, params[:id])
+
+ authorize_admin_source!(source_type, source)
+
+ source
+ end
+ end
+
+ %w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The ID of a #{source_type}"
+ end
+ resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ desc "Gets a list of #{source_type} badges viewable by the authenticated user." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ use :pagination
+ end
+ get ":id/badges" do
+ source = find_source(source_type, params[:id])
+
+ present_badges(source, paginate(source.badges))
+ end
+
+ desc "Preview a badge from a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::BasicBadgeDetails
+ end
+ params do
+ requires :link_url, type: String, desc: 'URL of the badge link'
+ requires :image_url, type: String, desc: 'URL of the badge image'
+ end
+ get ":id/badges/render" do
+ authenticate!
+
+ source = find_source_if_admin(source_type)
+
+ badge = ::Badges::BuildService.new(declared_params(include_missing: false))
+ .execute(source)
+
+ if badge.valid?
+ present_badges(source, badge, with: Entities::BasicBadgeDetails)
+ else
+ render_validation_error!(badge)
+ end
+ end
+
+ desc "Gets a badge of a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ requires :badge_id, type: Integer, desc: 'The badge ID'
+ end
+ get ":id/badges/:badge_id" do
+ source = find_source(source_type, params[:id])
+ badge = find_badge(source)
+
+ present_badges(source, badge)
+ end
+
+ desc "Adds a badge to a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ requires :link_url, type: String, desc: 'URL of the badge link'
+ requires :image_url, type: String, desc: 'URL of the badge image'
+ end
+ post ":id/badges" do
+ source = find_source_if_admin(source_type)
+
+ badge = ::Badges::CreateService.new(declared_params(include_missing: false)).execute(source)
+
+ if badge.persisted?
+ present_badges(source, badge)
+ else
+ render_validation_error!(badge)
+ end
+ end
+
+ desc "Updates a badge of a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ optional :link_url, type: String, desc: 'URL of the badge link'
+ optional :image_url, type: String, desc: 'URL of the badge image'
+ end
+ put ":id/badges/:badge_id" do
+ source = find_source_if_admin(source_type)
+
+ badge = ::Badges::UpdateService.new(declared_params(include_missing: false))
+ .execute(find_badge(source))
+
+ if badge.valid?
+ present_badges(source, badge)
+ else
+ render_validation_error!(badge)
+ end
+ end
+
+ desc 'Removes a badge from a project or group.' do
+ detail 'This feature was introduced in GitLab 10.6.'
+ end
+ params do
+ requires :badge_id, type: Integer, desc: 'The badge ID'
+ end
+ delete ":id/badges/:badge_id" do
+ source = find_source_if_admin(source_type)
+ badge = find_badge(source)
+
+ if badge.is_a?(GroupBadge) && source.is_a?(Project)
+ error!('To delete a Group badge please use the Group endpoint', 403)
+ end
+
+ destroy_conditionally!(badge)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0c8ec7dd5f5..e5bcbface6b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1235,5 +1235,23 @@ module API
expose :startline
expose :project_id
end
+
+ class BasicBadgeDetails < Grape::Entity
+ expose :link_url
+ expose :image_url
+ expose :rendered_link_url do |badge, options|
+ badge.rendered_link_url(options.fetch(:project, nil))
+ end
+ expose :rendered_image_url do |badge, options|
+ badge.rendered_image_url(options.fetch(:project, nil))
+ end
+ end
+
+ class Badge < BasicBadgeDetails
+ expose :id
+ expose :kind do |badge|
+ badge.type == 'ProjectBadge' ? 'project' : 'group'
+ end
+ end
end
end
diff --git a/lib/api/helpers/badges_helpers.rb b/lib/api/helpers/badges_helpers.rb
new file mode 100644
index 00000000000..1f8afbf3c90
--- /dev/null
+++ b/lib/api/helpers/badges_helpers.rb
@@ -0,0 +1,28 @@
+module API
+ module Helpers
+ module BadgesHelpers
+ include ::API::Helpers::MembersHelpers
+
+ def find_badge(source)
+ source.badges.find(params[:badge_id])
+ end
+
+ def present_badges(source, records, options = {})
+ entity_type = options[:with] || Entities::Badge
+ badge_params = badge_source_params(source).merge(with: entity_type)
+
+ present records, badge_params
+ end
+
+ def badge_source_params(source)
+ project = if source.is_a?(Project)
+ source
+ else
+ GroupProjectsFinder.new(group: source, current_user: current_user).execute.first
+ end
+
+ { project: project }
+ end
+ end
+ end
+end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 91cdc564002..7e6c33ec33d 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -204,6 +204,7 @@ module API
optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse))
optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse))
+ optional 'file.sha256', type: String, desc: %q(sha256 checksum of the file)
optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse))
end
@@ -224,7 +225,7 @@ module API
expire_in = params['expire_in'] ||
Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
- job.build_job_artifacts_archive(project: job.project, file_type: :archive, file: artifacts, expire_in: expire_in)
+ job.build_job_artifacts_archive(project: job.project, file_type: :archive, file: artifacts, file_sha256: params['file.sha256'], expire_in: expire_in)
job.build_job_artifacts_metadata(project: job.project, file_type: :metadata, file: metadata, expire_in: expire_in) if metadata
job.artifacts_expire_in = expire_in
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 86393ee254d..f5ccf952cf9 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -40,8 +40,8 @@ module Gitlab
end
def find_with_user_password(login, password)
- # Avoid resource intensive login checks if password is not provided
- return unless password.present?
+ # Avoid resource intensive checks if login credentials are not provided
+ return unless login.present? && password.present?
# Nothing to do here if internal auth is disabled and LDAP is
# not configured
@@ -50,14 +50,26 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
- # If no user is found, or it's an LDAP server, try LDAP.
- # LDAP users are only authenticated via LDAP
- if user.nil? || user.ldap_user?
- # Second chance - try LDAP authentication
- Gitlab::Auth::LDAP::Authentication.login(login, password)
- elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git?
- user if user.active? && user.valid_password?(password)
+ return if user && !user.active?
+
+ authenticators = []
+
+ if user
+ authenticators << Gitlab::Auth::OAuth::Provider.authentication(user, 'database')
+
+ # Add authenticators for all identities if user is not nil
+ user&.identities&.each do |identity|
+ authenticators << Gitlab::Auth::OAuth::Provider.authentication(user, identity.provider)
+ end
+ else
+ # If no user is provided, try LDAP.
+ # LDAP users are only authenticated via LDAP
+ authenticators << Gitlab::Auth::LDAP::Authentication
end
+
+ authenticators.compact!
+
+ user if authenticators.find { |auth| auth.login(login, password) }
end
end
diff --git a/lib/gitlab/auth/database/authentication.rb b/lib/gitlab/auth/database/authentication.rb
new file mode 100644
index 00000000000..260a77058a4
--- /dev/null
+++ b/lib/gitlab/auth/database/authentication.rb
@@ -0,0 +1,16 @@
+# These calls help to authenticate to OAuth provider by providing username and password
+#
+
+module Gitlab
+ module Auth
+ module Database
+ class Authentication < Gitlab::Auth::OAuth::Authentication
+ def login(login, password)
+ return false unless Gitlab::CurrentSettings.password_authentication_enabled_for_git?
+
+ user&.valid_password?(password)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/authentication.rb b/lib/gitlab/auth/ldap/authentication.rb
index cbb9cf4bb9c..e70c3ab6b46 100644
--- a/lib/gitlab/auth/ldap/authentication.rb
+++ b/lib/gitlab/auth/ldap/authentication.rb
@@ -7,7 +7,7 @@
module Gitlab
module Auth
module LDAP
- class Authentication
+ class Authentication < Gitlab::Auth::OAuth::Authentication
def self.login(login, password)
return unless Gitlab::Auth::LDAP::Config.enabled?
return unless login.present? && password.present?
@@ -28,11 +28,7 @@ module Gitlab
Gitlab::Auth::LDAP::Config.providers
end
- attr_accessor :provider, :ldap_user
-
- def initialize(provider)
- @provider = provider
- end
+ attr_accessor :ldap_user
def login(login, password)
@ldap_user = adapter.bind_as(
@@ -62,7 +58,7 @@ module Gitlab
end
def user
- return nil unless ldap_user
+ return unless ldap_user
Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
end
diff --git a/lib/gitlab/auth/o_auth/authentication.rb b/lib/gitlab/auth/o_auth/authentication.rb
new file mode 100644
index 00000000000..ed03b9f8b40
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/authentication.rb
@@ -0,0 +1,21 @@
+# These calls help to authenticate to OAuth provider by providing username and password
+#
+
+module Gitlab
+ module Auth
+ module OAuth
+ class Authentication
+ attr_reader :provider, :user
+
+ def initialize(provider, user = nil)
+ @provider = provider
+ @user = user
+ end
+
+ def login(login, password)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
index f8ab8ee1388..5fb61ffe00d 100644
--- a/lib/gitlab/auth/o_auth/provider.rb
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -8,11 +8,28 @@ module Gitlab
"google_oauth2" => "Google"
}.freeze
+ def self.authentication(user, provider)
+ return unless user
+ return unless enabled?(provider)
+
+ authenticator =
+ case provider
+ when /^ldap/
+ Gitlab::Auth::LDAP::Authentication
+ when 'database'
+ Gitlab::Auth::Database::Authentication
+ end
+
+ authenticator&.new(provider, user)
+ end
+
def self.providers
Devise.omniauth_providers
end
def self.enabled?(name)
+ return true if name == 'database'
+
providers.include?(name.to_sym)
end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index acd785bb02d..b6a96081278 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -161,7 +161,7 @@ module Gitlab
def find_by_uid_and_provider
identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
- identity && identity.user
+ identity&.user
end
def build_new_user
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 92f0e0402a8..a7e055ac444 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -20,6 +20,7 @@ module Gitlab
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.test_env = Rails.env.test?
+ gon.suggested_label_colors = LabelsHelper.suggested_colors
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index af203ff711d..b713fa7e1cd 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.2'.freeze
+ VERSION = '0.2.3'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 9f404003125..4bdd01f5e94 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -65,6 +65,7 @@ project_tree:
- :create_access_levels
- :project_feature
- :custom_attributes
+ - :project_badges
# Only include the following attributes for the models specified.
included_attributes:
@@ -125,6 +126,8 @@ excluded_attributes:
- :when
push_event_payload:
- :event_id
+ project_badges:
+ - :group_id
methods:
labels:
@@ -147,3 +150,5 @@ methods:
- :action
push_event_payload:
- :action
+ project_badges:
+ - :type
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 759833a5ee5..cf6b7e306dd 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -16,7 +16,8 @@ module Gitlab
priorities: :label_priorities,
auto_devops: :project_auto_devops,
label: :project_label,
- custom_attributes: 'ProjectCustomAttribute' }.freeze
+ custom_attributes: 'ProjectCustomAttribute',
+ project_badges: 'Badge' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index c26656704d7..d9d5f90596f 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -1,90 +1,19 @@
module Gitlab
module Middleware
class ReadOnly
- DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
- APPLICATION_JSON = 'application/json'.freeze
API_VERSIONS = (3..4)
+ def self.internal_routes
+ @internal_routes ||=
+ API_VERSIONS.map { |version| "api/v#{version}/internal" }
+ end
+
def initialize(app)
@app = app
- @whitelisted = internal_routes
end
def call(env)
- @env = env
- @route_hash = nil
-
- if disallowed_request? && Gitlab::Database.read_only?
- Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
- error_message = 'You cannot do writing operations on a read-only GitLab instance'
-
- if json_request?
- return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
- else
- rack_flash.alert = error_message
- rack_session['flash'] = rack_flash.to_session_value
-
- return [301, { 'Location' => last_visited_url }, []]
- end
- end
-
- @app.call(env)
- end
-
- private
-
- def internal_routes
- API_VERSIONS.flat_map { |version| "api/v#{version}/internal" }
- end
-
- def disallowed_request?
- DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !whitelisted_routes
- end
-
- def json_request?
- request.media_type == APPLICATION_JSON
- end
-
- def rack_flash
- @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
- end
-
- def rack_session
- @env['rack.session']
- end
-
- def request
- @env['rack.request'] ||= Rack::Request.new(@env)
- end
-
- def last_visited_url
- @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url
- end
-
- def route_hash
- @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
- end
-
- def whitelisted_routes
- grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
- end
-
- def sidekiq_route
- request.path.start_with?('/admin/sidekiq')
- end
-
- def grack_route
- # Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.path.end_with?('.git/git-upload-pack')
-
- route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
- end
-
- def lfs_route
- # Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.path.end_with?('/info/lfs/objects/batch')
-
- route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
+ ReadOnly::Controller.new(@app, env).call
end
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
new file mode 100644
index 00000000000..45b644e6510
--- /dev/null
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -0,0 +1,86 @@
+module Gitlab
+ module Middleware
+ class ReadOnly
+ class Controller
+ DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
+ APPLICATION_JSON = 'application/json'.freeze
+ ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'.freeze
+
+ def initialize(app, env)
+ @app = app
+ @env = env
+ end
+
+ def call
+ if disallowed_request? && Gitlab::Database.read_only?
+ Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
+
+ if json_request?
+ return [403, { 'Content-Type' => APPLICATION_JSON }, [{ 'message' => ERROR_MESSAGE }.to_json]]
+ else
+ rack_flash.alert = ERROR_MESSAGE
+ rack_session['flash'] = rack_flash.to_session_value
+
+ return [301, { 'Location' => last_visited_url }, []]
+ end
+ end
+
+ @app.call(@env)
+ end
+
+ private
+
+ def disallowed_request?
+ DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) &&
+ !whitelisted_routes
+ end
+
+ def json_request?
+ request.media_type == APPLICATION_JSON
+ end
+
+ def rack_flash
+ @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
+ end
+
+ def rack_session
+ @env['rack.session']
+ end
+
+ def request
+ @env['rack.request'] ||= Rack::Request.new(@env)
+ end
+
+ def last_visited_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url
+ end
+
+ def route_hash
+ @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
+ end
+
+ def whitelisted_routes
+ grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
+ end
+
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
+ end
+
+ def grack_route
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('.git/git-upload-pack')
+
+ route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
+ end
+
+ def lfs_route
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('/info/lfs/objects/batch')
+
+ route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/middleware/release_env.rb b/lib/gitlab/middleware/release_env.rb
new file mode 100644
index 00000000000..f8d0a135965
--- /dev/null
+++ b/lib/gitlab/middleware/release_env.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Middleware
+ # Some of middleware would hold env for no good reason even after the
+ # request had already been processed, and we could not garbage collect
+ # them due to this. Put this middleware as the first middleware so that
+ # it would clear the env after the request is done, allowing GC gets a
+ # chance to release memory for the last request.
+ ReleaseEnv = Struct.new(:app) do
+ def call(env)
+ app.call(env).tap { env.clear }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/string_placeholder_replacer.rb b/lib/gitlab/string_placeholder_replacer.rb
new file mode 100644
index 00000000000..9a2219b7d77
--- /dev/null
+++ b/lib/gitlab/string_placeholder_replacer.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ class StringPlaceholderReplacer
+ # This method accepts the following paras
+ # - string: the string to be analyzed
+ # - placeholder_regex: i.e. /%{project_path|project_id|default_branch|commit_sha}/
+ # - block: this block will be called with each placeholder found in the string using
+ # the placeholder regex. If the result of the block is nil, the original
+ # placeholder will be returned.
+
+ def self.replace_string_placeholders(string, placeholder_regex = nil, &block)
+ return string if string.blank? || placeholder_regex.blank? || !block_given?
+
+ replace_placeholders(string, placeholder_regex, &block)
+ end
+
+ class << self
+ private
+
+ # If the result of the block is nil, then the placeholder is returned
+ def replace_placeholders(string, placeholder_regex, &block)
+ string.gsub(/%{(#{placeholder_regex})}/) do |arg|
+ yield($~[1]) || arg
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c9b17f1e826..8a2176a4d72 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-03-01 18:03+0100\n"
-"PO-Revision-Date: 2018-03-01 18:03+0100\n"
+"POT-Creation-Date: 2018-03-05 13:02-0600\n"
+"PO-Revision-Date: 2018-03-05 13:02-0600\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -102,6 +102,9 @@ msgstr ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
+msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
+msgstr ""
+
msgid "About auto deploy"
msgstr ""
@@ -201,6 +204,9 @@ msgstr ""
msgid "All"
msgstr ""
+msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
+msgstr ""
+
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
@@ -698,6 +704,9 @@ msgstr ""
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
+msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -746,6 +755,9 @@ msgstr ""
msgid "ClusterIntegration|Ingress"
msgstr ""
+msgid "ClusterIntegration|Ingress IP Address"
+msgstr ""
+
msgid "ClusterIntegration|Install"
msgstr ""
@@ -1444,6 +1456,9 @@ msgstr ""
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
+msgid "Forking in progress"
+msgstr ""
+
msgid "Format"
msgstr ""
@@ -1462,6 +1477,9 @@ msgstr ""
msgid "Generate a default set of labels"
msgstr ""
+msgid "Git repository URL"
+msgstr ""
+
msgid "Git revision"
msgstr ""
@@ -1587,6 +1605,12 @@ msgstr ""
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
+msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgstr ""
+
+msgid "Import in progress"
+msgstr ""
+
msgid "Import repository"
msgstr ""
@@ -1823,6 +1847,9 @@ msgstr ""
msgid "Monitoring"
msgstr ""
+msgid "More information"
+msgstr ""
+
msgid "More information is available|here"
msgstr ""
@@ -2164,6 +2191,9 @@ msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
+msgid "Please wait while we import the repository for you. Refresh at will."
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -2520,6 +2550,9 @@ msgstr ""
msgid "Select target branch"
msgstr ""
+msgid "Send email"
+msgstr ""
+
msgid "Sep"
msgstr ""
@@ -2833,6 +2866,9 @@ msgstr ""
msgid "The fork relationship has been removed."
msgstr ""
+msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
+msgstr ""
+
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
@@ -2866,6 +2902,9 @@ msgstr ""
msgid "The repository for this project is empty"
msgstr ""
+msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
+msgstr ""
+
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
@@ -3131,6 +3170,9 @@ msgstr ""
msgid "Tip:"
msgstr ""
+msgid "To import an SVN repository, check out %{svn_link}."
+msgstr ""
+
msgid "Todo"
msgstr ""
@@ -3437,6 +3479,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "among other things"
+msgstr ""
+
msgid "assign yourself"
msgstr ""
@@ -3632,6 +3677,9 @@ msgstr ""
msgid "spendCommand|%{slash_command} will update the sum of the time spent."
msgstr ""
+msgid "this document"
+msgstr ""
+
msgid "username"
msgstr ""
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index b7257fac608..fb6d82d7de3 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -246,7 +246,7 @@ describe AutocompleteController do
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq authorized_project.id
- expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace
+ expect(json_response.first['name_with_namespace']).to eq authorized_project.full_name
end
end
end
@@ -267,7 +267,7 @@ describe AutocompleteController do
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq authorized_search_project.id
- expect(json_response.first['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+ expect(json_response.first['name_with_namespace']).to eq authorized_search_project.full_name
end
end
end
diff --git a/spec/factories/badge.rb b/spec/factories/badge.rb
new file mode 100644
index 00000000000..b87ece946cb
--- /dev/null
+++ b/spec/factories/badge.rb
@@ -0,0 +1,14 @@
+FactoryBot.define do
+ trait :base_badge do
+ link_url { generate(:url) }
+ image_url { generate(:url) }
+ end
+
+ factory :project_badge, traits: [:base_badge], class: ProjectBadge do
+ project
+ end
+
+ factory :group_badge, aliases: [:badge], traits: [:base_badge], class: GroupBadge do
+ group
+ end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index a5f22848031..d5e603baeae 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -173,7 +173,7 @@ feature 'Admin Groups' do
visit admin_group_path(group)
- expect(page).to have_content(empty_project.name_with_namespace)
+ expect(page).to have_content(empty_project.full_name)
expect(page).to have_content('Projects shared with')
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index d02ac6c2e2a..6d8350e99f1 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -58,7 +58,7 @@ describe "Admin::Projects" do
expect(current_path).to eq admin_project_path(project)
expect(page).to have_content(project.path)
expect(page).to have_content(project.name)
- expect(page).to have_content(project.name_with_namespace)
+ expect(page).to have_content(project.full_name)
expect(page).to have_content(project.creator.name)
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 7eeed7da998..8de2e3d199b 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -76,8 +76,8 @@ describe "Admin Runners" do
describe 'projects' do
it 'contains project names' do
- expect(page).to have_content(@project1.name_with_namespace)
- expect(page).to have_content(@project2.name_with_namespace)
+ expect(page).to have_content(@project1.full_name)
+ expect(page).to have_content(@project2.full_name)
end
end
@@ -89,8 +89,8 @@ describe "Admin Runners" do
end
it 'contains name of correct project' do
- expect(page).to have_content(@project1.name_with_namespace)
- expect(page).not_to have_content(@project2.name_with_namespace)
+ expect(page).to have_content(@project1.full_name)
+ expect(page).not_to have_content(@project2.full_name)
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 54652e2d849..8d1d5a51750 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -74,8 +74,8 @@ RSpec.describe 'Dashboard Issues' do
find('.new-project-item-select-button').click
page.within('.select2-results') do
- expect(page).to have_content(project.name_with_namespace)
- expect(page).not_to have_content(project_with_issues_disabled.name_with_namespace)
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
end
end
@@ -84,8 +84,8 @@ RSpec.describe 'Dashboard Issues' do
wait_for_requests
- project_path = "/#{project.path_with_namespace}"
- project_json = { name: project.name_with_namespace, url: project_path }.to_json
+ project_path = "/#{project.full_path}"
+ project_json = { name: project.full_name, url: project_path }.to_json
# simulate selection, and prevent overlap by dropdown menu
first('.project-item-select', visible: false)
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 744041ac425..c8f3a8449f5 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -28,8 +28,8 @@ feature 'Dashboard Merge Requests' do
find('.new-project-item-select-button').click
page.within('.select2-results') do
- expect(page).to have_content(project.name_with_namespace)
- expect(page).not_to have_content(project_with_disabled_merge_requests.name_with_namespace)
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_disabled_merge_requests.full_name)
end
end
end
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 2fc34301d51..7b359b0c651 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -24,14 +24,14 @@ feature 'Dashboard > User filters todos', :js do
it 'filters by project' do
click_button 'Project'
within '.dropdown-menu-project' do
- fill_in 'Search projects', with: project_1.name_with_namespace
- click_link project_1.name_with_namespace
+ fill_in 'Search projects', with: project_1.full_name
+ click_link project_1.full_name
end
wait_for_requests
- expect(page).to have_content project_1.name_with_namespace
- expect(page).not_to have_content project_2.name_with_namespace
+ expect(page).to have_content project_1.full_name
+ expect(page).not_to have_content project_2.full_name
end
context 'Author filter' do
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 076a02150a4..3c01ff345fc 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -73,7 +73,7 @@ feature 'issue move to another project' do
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
- expect(page).to have_content new_project.name_with_namespace
+ expect(page).to have_content new_project.full_name
end
end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 0cc68aff494..12bfcc177c7 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index d575596937d..1f4eec0a317 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Projects > Members > Master manages access requests' do
perform_enqueued_jobs { click_on 'Grant access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was granted"
end
scenario 'master can deny access' do
@@ -36,7 +36,7 @@ feature 'Projects > Members > Master manages access requests' do
perform_enqueued_jobs { click_on 'Deny access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was denied"
end
def expect_visible_access_request(project, user)
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 4eb36156812..672d5daa3d8 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -21,7 +21,7 @@ feature 'Projects > Members > User requests access', :js do
perform_enqueued_jobs { click_link 'Request Access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
+ expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.full_name} project"
expect(project.requesters.exists?(user_id: user)).to be_truthy
expect(page).to have_content 'Your request for access has been queued for review.'
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index b5104747d00..fd561288091 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -142,7 +142,7 @@ feature 'New project' do
context 'from git repository url, "Repo by URL"' do
before do
- first('.import_git').click
+ first('.js-import-git-toggle-button').click
end
it 'does not autocomplete sensitive git repo URL' do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 3a8e7c05cc4..849d85061df 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -86,7 +86,22 @@ describe 'Pipelines', :js do
it 'updates content when tab is clicked' do
page.find('.js-pipelines-tab-pending').click
wait_for_requests
- expect(page).to have_content('No pipelines to show.')
+ expect(page).to have_content('There are currently no pending pipelines.')
+ end
+ end
+
+ context 'navigation links' do
+ before do
+ visit project_pipelines_path(project)
+ wait_for_requests
+ end
+
+ it 'renders run pipeline link' do
+ expect(page).to have_link('Run Pipeline')
+ end
+
+ it 'renders ci lint link' do
+ expect(page).to have_link('CI Lint')
end
end
@@ -542,7 +557,7 @@ describe 'Pipelines', :js do
end
it 'has a clear caches button' do
- expect(page).to have_link 'Clear runner caches'
+ expect(page).to have_link 'Clear Runner Caches'
end
describe 'user clicks the button' do
@@ -552,19 +567,31 @@ describe 'Pipelines', :js do
end
it 'increments jobs_cache_index' do
- click_link 'Clear runner caches'
+ click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end
end
context 'when project does not have jobs_cache_index' do
it 'sets jobs_cache_index to 1' do
- click_link 'Clear runner caches'
+ click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end
end
end
end
+
+ describe 'Empty State' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ visit project_pipelines_path(project)
+ end
+
+ it 'renders empty state' do
+ expect(page).to have_content 'Build with confidence'
+ end
+ end
end
context 'when user is not logged in' do
@@ -575,7 +602,9 @@ describe 'Pipelines', :js do
context 'when project is public' do
let(:project) { create(:project, :public, :repository) }
- it { expect(page).to have_content 'Build with confidence' }
+ context 'without pipelines' do
+ it { expect(page).to have_content 'This project is not currently set up to run pipelines.' }
+ end
end
context 'when project is private' do
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 2709047b8de..0a4f57bcd21 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -39,7 +39,7 @@ describe 'User manages project members' do
click_link('Import')
end
- select(project2.name_with_namespace, from: 'source_project_id')
+ select(project2.full_name, from: 'source_project_id')
click_button('Import')
project_member = project.project_members.find_by(user_id: user_mike.id)
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 77212fb105b..9e089c5a6cb 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -35,7 +35,7 @@ describe 'User searches for code' do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: 'rspec')
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index ef9553f2a91..d6120ff8517 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -34,7 +34,7 @@ describe 'User searches for issues', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: issue1.title)
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 3b6739aecbd..68e2f7a857d 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -33,7 +33,7 @@ describe 'User searches for merge requests', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: merge_request1.title)
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 6e197aee498..fc6cd81eb68 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -33,7 +33,7 @@ describe 'User searches for milestones', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: milestone1.title)
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 00af625dc86..7934779058f 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -18,7 +18,7 @@ describe 'User searches for wiki pages', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: 'content')
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index aa883c964d2..66afe163447 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -31,7 +31,7 @@ describe 'User uses search filters', :js do
wait_for_requests
- expect(page).to have_link(group_project.name_with_namespace)
+ expect(page).to have_link(group_project.full_name)
end
end
end
@@ -43,10 +43,10 @@ describe 'User uses search filters', :js do
wait_for_requests
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
- expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ expect(find('.js-search-project-dropdown')).to have_content(project.full_name)
end
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 45ffbeb27a4..4590904c93d 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -12,10 +12,10 @@ describe MembersHelper do
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
- it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
- it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
- it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
- it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.full_name} project?" }
+ it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" }
+ it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" }
+ it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" }
it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
@@ -42,7 +42,7 @@ describe MembersHelper do
let(:group) { build_stubbed(:group) }
let(:user) { build_stubbed(:user) }
- it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
+ it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.full_name}\" project?" }
it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
end
end
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index f55163c26e9..63806ef91f3 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -26,8 +26,8 @@ describe TodosHelper do
expected_results = [
{ 'id' => '', 'text' => 'Any Project' },
- { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace },
- { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace }
+ { 'id' => projects.second.id, 'text' => projects.second.full_name },
+ { 'id' => projects.first.id, 'text' => projects.first.full_name }
]
expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results)
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 80a598e63bd..13d607a06d2 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -9,8 +9,8 @@ import axios from '~/lib/utils/axios_utils';
import '~/boards/models/assignee';
import eventHub from '~/boards/eventhub';
+import '~/vue_shared/models/label';
import '~/boards/models/list';
-import '~/boards/models/label';
import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 8411f4dd8a6..0cf9e4c9ba1 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 278155c585e..37088a6421c 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -4,8 +4,8 @@
import Vue from 'vue';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/stores/boards_store';
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index dbbe14fe3e0..4a11131b55c 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -3,8 +3,8 @@
/* global ListIssue */
import Vue from 'vue';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 34964b20b05..d9a1d692949 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -6,8 +6,8 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 7eecb58a4c3..e9d77f035e3 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -1,7 +1,7 @@
/* global ListIssue */
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/stores/modal_store';
diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/javascripts/pipelines/blank_state_spec.js
new file mode 100644
index 00000000000..b7a9b60d85c
--- /dev/null
+++ b/spec/javascripts/pipelines/blank_state_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import component from '~/pipelines/components/blank_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Pipelines Blank State', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(component);
+
+ vm = mountComponent(Component,
+ {
+ svgPath: 'foo',
+ message: 'Blank State',
+ },
+ );
+ });
+
+ it('should render svg', () => {
+ expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
+ });
+
+ it('should render message', () => {
+ expect(
+ vm.$el.querySelector('h4').textContent.trim(),
+ ).toEqual('Blank State');
+ });
+});
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index 97f04844b3a..71f77e5f42e 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import emptyStateComp from '~/pipelines/components/empty_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Empty State', () => {
let component;
@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => {
beforeEach(() => {
EmptyStateComponent = Vue.extend(emptyStateComp);
- component = new EmptyStateComponent({
- propsData: {
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- },
- }).$mount();
+ component = mountComponent(EmptyStateComponent, {
+ helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ canSetCi: true,
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
});
it('should render empty state SVG', () => {
@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect(
- component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '),
- ).toContain('Continous Integration can help catch bugs by running your tests automatically');
+ component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
+ ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
expect(
- component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '),
- ).toContain('Continuous Deployment can help you deliver code to your product environment');
+ component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
+ ).toContain('while Continuous Deployment can help you deliver code to your product environment');
});
it('should render a link with provided help path', () => {
- expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo');
- expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
+ expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines');
});
});
diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js
deleted file mode 100644
index a402857a4d1..00000000000
--- a/spec/javascripts/pipelines/error_state_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import errorStateComp from '~/pipelines/components/error_state.vue';
-
-describe('Pipelines Error State', () => {
- let component;
- let ErrorStateComponent;
-
- beforeEach(() => {
- ErrorStateComponent = Vue.extend(errorStateComp);
-
- component = new ErrorStateComponent({
- propsData: {
- errorStateSvgPath: 'foo',
- },
- }).$mount();
- });
-
- it('should render error state SVG', () => {
- expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
- });
-
- it('should render emtpy state information', () => {
- expect(
- component.$el.querySelector('h4').textContent,
- ).toContain('The API failed to fetch the pipelines');
- });
-});
diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js
index 09a0c14d96c..77c5258f74c 100644
--- a/spec/javascripts/pipelines/nav_controls_spec.js
+++ b/spec/javascripts/pipelines/nav_controls_spec.js
@@ -1,116 +1,68 @@
import Vue from 'vue';
import navControlsComp from '~/pipelines/components/nav_controls.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Nav Controls', () => {
let NavControlsComponent;
+ let component;
beforeEach(() => {
NavControlsComponent = Vue.extend(navControlsComp);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
it('should render link to create a new pipeline', () => {
const mockData = {
newPipelinePath: 'foo',
- hasCiEnabled: true,
- helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: true,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline');
- expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath);
+ expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
+ expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath);
});
- it('should not render link to create pipeline if no permission is provided', () => {
+ it('should not render link to create pipeline if no path is provided', () => {
const mockData = {
- newPipelinePath: 'foo',
- hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: false,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelector('.btn-create')).toEqual(null);
+ expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
});
it('should render link for resetting runner caches', () => {
const mockData = {
newPipelinePath: 'foo',
- hasCiEnabled: true,
- helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: false,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches');
- expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath);
+ expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches');
+ expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath);
});
it('should render link for CI lint', () => {
const mockData = {
newPipelinePath: 'foo',
- hasCiEnabled: true,
- helpPagePath: 'foo',
- ciLintPath: 'foo',
- resetCachePath: 'foo',
- canCreatePipeline: true,
- };
-
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
-
- expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint');
- expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath);
- });
-
- it('should render link to help page when CI is not enabled', () => {
- const mockData = {
- newPipelinePath: 'foo',
- hasCiEnabled: false,
- helpPagePath: 'foo',
- ciLintPath: 'foo',
- resetCachePath: 'foo',
- canCreatePipeline: true,
- };
-
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
-
- expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
- expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
- });
-
- it('should not render link to help page when CI is enabled', () => {
- const mockData = {
- newPipelinePath: 'foo',
- hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: true,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelector('.btn-info')).toEqual(null);
+ expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
+ expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath);
});
});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index 54d5bfd51e6..84fd0329f08 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
- preloadFixtures('static/pipelines.html.raw');
preloadFixtures(jsonFixtureName);
let PipelinesComponent;
let pipelines;
- let component;
+ let vm;
+ const paths = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ ciLintPath: '/ci/lint',
+ resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
+ newPipelinePath: '/twitter/flight/pipelines/new',
+ };
+
+ const noPermissions = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ };
beforeEach(() => {
- loadFixtures('static/pipelines.html.raw');
pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp);
});
afterEach(() => {
- component.$destroy();
+ vm.$destroy();
+ });
+
+ const pipelinesInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(pipelines), {
+ status: 200,
+ }));
+ };
+
+ const emptyStateInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ }), {
+ status: 200,
+ }));
+ };
+
+ const errorInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({}), {
+ status: 500,
+ }));
+ };
+
+ describe('With permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(pipelinesInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('renders Run Pipeline button', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint button', () => {
+ expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
+ });
+
+ it('renders pipelines table', () => {
+ expect(
+ vm.$el.querySelectorAll('.gl-responsive-table-row').length,
+ ).toEqual(pipelines.pipelines.length + 1);
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('renders Run Pipeline button', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint button', () => {
+ expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
+ });
+
+ it('renders tab empty state', () => {
+ expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+
+ it('renders empty state', () => {
+ expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence');
+ expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath);
+ });
+
+ it('does not render tabs nor buttons', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(errorInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, errorInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('renders buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
+ expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
+ expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
+ });
+
+ it('renders error state', () => {
+ expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
+ });
+ });
+ });
+
+ describe('Without permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(pipelinesInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: false,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+
+ it('renders pipelines table', () => {
+ expect(
+ vm.$el.querySelectorAll('.gl-responsive-table-row').length,
+ ).toEqual(pipelines.pipelines.length + 1);
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: false,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+
+ it('renders tab empty state', () => {
+ expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: false,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+
+ it('renders empty state without button to set CI', () => {
+ expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.');
+ expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull();
+ });
+
+ it('does not render tabs or buttons', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(errorInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: true,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, errorInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('does not renders buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+
+ it('renders error state', () => {
+ expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
+ });
+ });
});
describe('successfull request', () => {
describe('with pipelines', () => {
- const pipelinesInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(pipelines), {
- status: 200,
- }));
- };
-
beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor);
- component = mountComponent(PipelinesComponent, {
+ vm = mountComponent(PipelinesComponent, {
store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
});
});
@@ -48,9 +392,9 @@ describe('Pipelines', () => {
it('should render table', (done) => {
setTimeout(() => {
- expect(component.$el.querySelector('.table-holder')).toBeDefined();
+ expect(vm.$el.querySelector('.table-holder')).toBeDefined();
expect(
- component.$el.querySelectorAll('.gl-responsive-table-row').length,
+ vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
done();
});
@@ -59,22 +403,22 @@ describe('Pipelines', () => {
it('should render navigation tabs', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
).toContain('Pending');
expect(
- component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
).toContain('All');
expect(
- component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
).toContain('Running');
expect(
- component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
).toContain('Finished');
expect(
- component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
).toContain('Branches');
expect(
- component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
).toContain('Tags');
done();
});
@@ -82,10 +426,10 @@ describe('Pipelines', () => {
it('should make an API request when using tabs', (done) => {
setTimeout(() => {
- spyOn(component, 'updateContent');
- component.$el.querySelector('.js-pipelines-tab-finished').click();
+ spyOn(vm, 'updateContent');
+ vm.$el.querySelector('.js-pipelines-tab-finished').click();
- expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
done();
});
});
@@ -93,9 +437,9 @@ describe('Pipelines', () => {
describe('with pagination', () => {
it('should make an API request when using pagination', (done) => {
setTimeout(() => {
- spyOn(component, 'updateContent');
+ spyOn(vm, 'updateContent');
// Mock pagination
- component.store.state.pageInfo = {
+ vm.store.state.pageInfo = {
page: 1,
total: 10,
perPage: 2,
@@ -103,9 +447,9 @@ describe('Pipelines', () => {
totalPages: 5,
};
- Vue.nextTick(() => {
- component.$el.querySelector('.js-next-button a').click();
- expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.js-next-button a').click();
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
done();
});
@@ -113,112 +457,249 @@ describe('Pipelines', () => {
});
});
});
+ });
- describe('without pipelines', () => {
- const emptyInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
+ describe('methods', () => {
+ beforeEach(() => {
+ spyOn(history, 'pushState').and.stub();
+ });
- beforeEach(() => {
- Vue.http.interceptors.push(emptyInterceptor);
- });
+ describe('updateContent', () => {
+ it('should set given parameters', () => {
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+ vm.updateContent({ scope: 'finished', page: '4' });
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, emptyInterceptor,
- );
+ expect(vm.page).toEqual('4');
+ expect(vm.scope).toEqual('finished');
+ expect(vm.requestData.scope).toEqual('finished');
+ expect(vm.requestData.page).toEqual('4');
});
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+ spyOn(vm, 'updateContent');
- it('should render empty state', (done) => {
- component = new PipelinesComponent({
- propsData: {
- store: new Store(),
- },
- }).$mount();
+ vm.onChangeTab('running');
- setTimeout(() => {
- expect(component.$el.querySelector('.empty-state')).not.toBe(null);
- done();
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
});
+ spyOn(vm, 'updateContent');
+
+ vm.onChangePage(4);
+
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' });
});
});
});
- describe('unsuccessfull request', () => {
- const errorInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
+ describe('computed properties', () => {
beforeEach(() => {
- Vue.http.interceptors.push(errorInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, errorInterceptor,
- );
+ describe('tabs', () => {
+ it('returns default tabs', () => {
+ expect(vm.tabs).toEqual([
+ { name: 'All', scope: 'all', count: undefined, isActive: true },
+ { name: 'Pending', scope: 'pending', count: undefined, isActive: false },
+ { name: 'Running', scope: 'running', count: undefined, isActive: false },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
+ });
});
- it('should render error state', (done) => {
- component = new PipelinesComponent({
- propsData: {
- store: new Store(),
- },
- }).$mount();
+ describe('emptyTabMessage', () => {
+ it('returns message with scope', (done) => {
+ vm.scope = 'pending';
- setTimeout(() => {
- expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
- done();
+ vm.$nextTick(() => {
+ expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
+ done();
+ });
});
- });
- });
- describe('methods', () => {
- beforeEach(() => {
- spyOn(history, 'pushState').and.stub();
+ it('returns message without scope when scope is `all`', () => {
+ expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
+ });
});
- describe('updateContent', () => {
- it('should set given parameters', () => {
- component = mountComponent(PipelinesComponent, {
- store: new Store(),
+ describe('stateToRender', () => {
+ it('returns loading state when the app is loading', () => {
+ expect(vm.stateToRender).toEqual('loading');
+ });
+
+ it('returns error state when app has error', (done) => {
+ vm.hasError = true;
+ vm.isLoading = false;
+
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('error');
+ done();
+ });
+ });
+
+ it('returns table list when app has pipelines', (done) => {
+ vm.isLoading = false;
+ vm.hasError = false;
+ vm.state.pipelines = pipelines.pipelines;
+
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('tableList');
+
+ done();
+ });
+ });
+
+ it('returns empty tab when app does not have pipelines but project has pipelines', (done) => {
+ vm.state.count.all = 10;
+ vm.isLoading = false;
+
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('emptyTab');
+
+ done();
});
- component.updateContent({ scope: 'finished', page: '4' });
+ });
+
+ it('returns empty tab when project has CI', (done) => {
+ vm.isLoading = false;
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('emptyTab');
- expect(component.page).toEqual('4');
- expect(component.scope).toEqual('finished');
- expect(component.requestData.scope).toEqual('finished');
- expect(component.requestData.page).toEqual('4');
+ done();
+ });
+ });
+
+ it('returns empty state when project does not have pipelines nor CI', (done) => {
+ vm.isLoading = false;
+ vm.hasGitlabCi = false;
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('emptyState');
+
+ done();
+ });
});
});
- describe('onChangeTab', () => {
- it('should set page to 1', () => {
- component = mountComponent(PipelinesComponent, {
- store: new Store(),
+ describe('shouldRenderTabs', () => {
+ it('returns true when state is loading & has already made the first request', (done) => {
+ vm.isLoading = true;
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
});
- spyOn(component, 'updateContent');
+ });
- component.onChangeTab('running');
+ it('returns true when state is tableList & has already made the first request', (done) => {
+ vm.isLoading = false;
+ vm.state.pipelines = pipelines.pipelines;
+ vm.hasMadeRequest = true;
- expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
+ });
+ });
+
+ it('returns true when state is error & has already made the first request', (done) => {
+ vm.isLoading = false;
+ vm.hasError = true;
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
+ });
+ });
+
+ it('returns true when state is empty tab & has already made the first request', (done) => {
+ vm.isLoading = false;
+ vm.state.count.all = 10;
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
+ });
+ });
+
+ it('returns false when has not made first request', (done) => {
+ vm.hasMadeRequest = false;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(false);
+
+ done();
+ });
+ });
+
+ it('returns false when state is emtpy state', (done) => {
+ vm.isLoading = false;
+ vm.hasMadeRequest = true;
+ vm.hasGitlabCi = false;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(false);
+
+ done();
+ });
});
});
- describe('onChangePage', () => {
- it('should update page and keep scope', () => {
- component = mountComponent(PipelinesComponent, {
- store: new Store(),
+ describe('shouldRenderButtons', () => {
+ it('returns true when it has paths & has made the first request', (done) => {
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderButtons).toEqual(true);
+
+ done();
});
- spyOn(component, 'updateContent');
+ });
+
+ it('returns false when it has not made the first request', (done) => {
+ vm.hasMadeRequest = false;
- component.onChangePage(4);
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderButtons).toEqual(false);
- expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
new file mode 100644
index 00000000000..67056793a20
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+
+import LabelsSelect from '~/labels_select';
+import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
+
+import { mockConfig, mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (config = mockConfig) => {
+ const Component = Vue.extend(baseComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('BaseComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hiddenInputName', () => {
+ it('returns correct string when showCreate prop is `true`', () => {
+ expect(vm.hiddenInputName).toBe('issue[label_names][]');
+ });
+
+ it('returns correct string when showCreate prop is `false`', () => {
+ const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false });
+ const vmNonEditable = createComponent(mockConfigNonEditable);
+ expect(vmNonEditable.hiddenInputName).toBe('label_id[]');
+ vmNonEditable.$destroy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('emits onLabelClick event with label and list of labels as params', () => {
+ spyOn(vm, '$emit');
+ vm.handleClick(mockLabels[0]);
+ expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
+ expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `block labels`', () => {
+ expect(vm.$el.classList.contains('block')).toBe(true);
+ expect(vm.$el.classList.contains('labels')).toBe(true);
+ });
+
+ it('renders `.selectbox` element', () => {
+ expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
+ expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
+ });
+
+ it('renders `.dropdown` element', () => {
+ expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
+ });
+
+ it('renders `.dropdown-menu` element', () => {
+ const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
+ expect(dropdownMenuEl).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
new file mode 100644
index 00000000000..ec63ac306d0
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+
+import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
+
+import { mockConfig, mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const componentConfig = Object.assign({}, mockConfig, {
+ fieldName: 'label_id[]',
+ labels: mockLabels,
+ showExtraOptions: false,
+});
+
+const createComponent = (config = componentConfig) => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns text as `Label` when `labels` prop is empty array', () => {
+ const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] });
+ const vmEmptyLabels = createComponent(mockEmptyLabels);
+ expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
+ const mockMoreLabels = Object.assign({}, componentConfig, {
+ labels: mockLabels.concat(mockLabels),
+ });
+ const vmMoreLabels = createComponent(mockMoreLabels);
+ expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more');
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns first label name when `labels` prop has only one item present', () => {
+ expect(vm.dropdownToggleText).toBe('Foo Label');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label');
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('i.fa');
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
new file mode 100644
index 00000000000..f07aefb2f87
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+
+import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
+
+import { mockSuggestedColors } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownCreateLabelComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownCreateLabelComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ gon.suggested_label_colors = mockSuggestedColors;
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('created', () => {
+ it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
+ expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
+ expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
+ });
+
+ it('renders `Go back` button on component header', () => {
+ const backButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-back');
+ expect(backButtonEl).not.toBe(null);
+ expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null);
+ });
+
+ it('renders component header element', () => {
+ const headerEl = vm.$el.querySelector('.dropdown-title');
+ expect(headerEl.innerText.trim()).toContain('Create new label');
+ });
+
+ it('renders `Close` button on component header', () => {
+ const closeButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close');
+ expect(closeButtonEl).not.toBe(null);
+ expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null);
+ });
+
+ it('renders `Name new label` input element', () => {
+ expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
+ expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
+ });
+
+ it('renders suggested colors list elements', () => {
+ const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
+ expect(colorsListContainerEl).not.toBe(null);
+ expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
+
+ const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
+ expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
+ expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
+ });
+
+ it('renders color input element', () => {
+ expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
+ expect(vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview')).not.toBe(null);
+ expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
+ });
+
+ it('renders component action buttons', () => {
+ const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
+ const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
+ expect(createBtnEl).not.toBe(null);
+ expect(createBtnEl.innerText.trim()).toBe('Create');
+ expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
new file mode 100644
index 00000000000..809e0327b1c
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+
+import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
+
+import { mockConfig } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (labelsWebUrl = mockConfig.labelsWebUrl) => {
+ const Component = Vue.extend(dropdownFooterComponent);
+
+ return mountComponent(Component, {
+ labelsWebUrl,
+ });
+};
+
+describe('DropdownFooterComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders `Create new label` link element', () => {
+ const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
+ expect(createLabelEl).not.toBeNull();
+ expect(createLabelEl.innerText.trim()).toBe('Create new label');
+ });
+
+ it('renders `Manage labels` link element', () => {
+ const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
+ expect(manageLabelsEl).not.toBeNull();
+ expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
+ expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
new file mode 100644
index 00000000000..325fa47c957
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+
+import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownHeaderComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownHeaderComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders header text element', () => {
+ const headerEl = vm.$el.querySelector('.dropdown-title span');
+ expect(headerEl.innerText.trim()).toBe('Assign labels');
+ });
+
+ it('renders `Close` button element', () => {
+ const closeBtnEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close');
+ expect(closeBtnEl).not.toBeNull();
+ expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
new file mode 100644
index 00000000000..703b87498c7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+
+import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue';
+
+import { mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (name = 'label_id[]', label = mockLabels[0]) => {
+ const Component = Vue.extend(dropdownHiddenInputComponent);
+
+ return mountComponent(Component, {
+ name,
+ label,
+ });
+};
+
+describe('DropdownHiddenInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element of type `hidden`', () => {
+ expect(vm.$el.nodeName).toBe('INPUT');
+ expect(vm.$el.getAttribute('type')).toBe('hidden');
+ expect(vm.$el.getAttribute('name')).toBe(vm.name);
+ expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
new file mode 100644
index 00000000000..69e11d966c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+
+import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownSearchInputComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element with type `search`', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+ expect(inputEl).not.toBeNull();
+ expect(inputEl.getAttribute('type')).toBe('search');
+ });
+
+ it('renders search icon element', () => {
+ expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
+ });
+
+ it('renders clear search icon element', () => {
+ expect(vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
new file mode 100644
index 00000000000..c3580933072
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+
+import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (canEdit = true) => {
+ const Component = Vue.extend(dropdownTitleComponent);
+
+ return mountComponent(Component, {
+ canEdit,
+ });
+};
+
+describe('DropdownTitleComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders title text', () => {
+ expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
+ expect(vm.$el.innerText.trim()).toContain('Labels');
+ });
+
+ it('renders spinner icon element', () => {
+ expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull();
+ });
+
+ it('renders `Edit` button element', () => {
+ const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
+ expect(editBtnEl).not.toBeNull();
+ expect(editBtnEl.innerText.trim()).toBe('Edit');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
new file mode 100644
index 00000000000..93b42795bea
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -0,0 +1,74 @@
+import Vue from 'vue';
+
+import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
+
+import { mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (labels = mockLabels) => {
+ const Component = Vue.extend(dropdownValueCollapsedComponent);
+
+ return mountComponent(Component, {
+ labels,
+ });
+};
+
+describe('DropdownValueCollapsedComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('labelsList', () => {
+ it('returns empty text when `labels` prop is empty array', () => {
+ const vmEmptyLabels = createComponent([]);
+ expect(vmEmptyLabels.labelsList).toBe('');
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns labels names separated by coma when `labels` prop has more than one item', () => {
+ const vmMoreLabels = createComponent(mockLabels.concat(mockLabels));
+ expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label');
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
+ const mockMoreLabels = Object.assign([], mockLabels);
+ for (let i = 0; i < 6; i += 1) {
+ mockMoreLabels.unshift(mockLabels[0]);
+ }
+
+ const vmMoreLabels = createComponent(mockMoreLabels);
+ expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more');
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns first label name when `labels` prop has only one item present', () => {
+ expect(vm.labelsList).toBe('Foo Label');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with tooltip`', () => {
+ expect(vm.$el.dataset.placement).toBe('left');
+ expect(vm.$el.dataset.container).toBe('body');
+ expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList);
+ });
+
+ it('renders tags icon element', () => {
+ expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
+ });
+
+ it('renders labels count', () => {
+ expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
new file mode 100644
index 00000000000..66e0957b431
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -0,0 +1,94 @@
+import Vue from 'vue';
+
+import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
+
+import { mockConfig, mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (
+ labels = mockLabels,
+ labelFilterBasePath = mockConfig.labelFilterBasePath,
+) => {
+ const Component = Vue.extend(dropdownValueComponent);
+
+ return mountComponent(Component, {
+ labels,
+ labelFilterBasePath,
+ });
+};
+
+describe('DropdownValueComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isEmpty', () => {
+ it('returns true if `labels` prop is empty', () => {
+ const vmEmptyLabels = createComponent([]);
+ expect(vmEmptyLabels.isEmpty).toBe(true);
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns false if `labels` prop is empty', () => {
+ expect(vm.isEmpty).toBe(false);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('labelFilterUrl', () => {
+ it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
+ expect(vm.labelFilterUrl({
+ title: 'Foo bar',
+ })).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar');
+ });
+ });
+
+ describe('labelStyle', () => {
+ it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
+ const label = {
+ textColor: '#FFFFFF',
+ color: '#BADA55',
+ };
+ const styleObj = vm.labelStyle(label);
+
+ expect(styleObj.color).toBe(label.textColor);
+ expect(styleObj.backgroundColor).toBe(label.color);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
+ expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(true);
+ });
+
+ it('render slot content inside component when `labels` prop is empty', () => {
+ const vmEmptyLabels = createComponent([]);
+ expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(mockConfig.emptyValueText);
+ vmEmptyLabels.$destroy();
+ });
+
+ it('renders label element with filter URL', () => {
+ expect(vm.$el.querySelector('a').getAttribute('href')).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20Label');
+ });
+
+ it('renders label element with tooltip and styles based on label details', () => {
+ const labelEl = vm.$el.querySelector('a span.label.color-label');
+ expect(labelEl).not.toBeNull();
+ expect(labelEl.dataset.placement).toBe('bottom');
+ expect(labelEl.dataset.container).toBe('body');
+ expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description);
+ expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
+ expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
new file mode 100644
index 00000000000..e9008c29b22
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -0,0 +1,49 @@
+export const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+export const mockSuggestedColors = [
+ '#0033CC',
+ '#428BCA',
+ '#44AD8E',
+ '#A8D695',
+ '#5CB85C',
+ '#69D100',
+ '#004E00',
+ '#34495E',
+ '#7F8C8D',
+ '#A295D6',
+ '#5843AD',
+ '#8E44AD',
+ '#FFECDB',
+ '#AD4363',
+ '#D10069',
+ '#CC0033',
+ '#FF0000',
+ '#D9534F',
+ '#D1D100',
+ '#F0AD4E',
+ '#AD8D43',
+];
+
+export const mockConfig = {
+ showCreate: true,
+ abilityName: 'issue',
+ context: {
+ labels: mockLabels,
+ },
+ namespace: 'gitlab-org',
+ updatePath: '/gitlab-org/my-project/issue/1',
+ labelsPath: '/gitlab-org/my-project/labels.json',
+ labelsWebUrl: '/gitlab-org/my-project/labels',
+ labelFilterBasePath: '/gitlab-org/my-project/issues',
+ canEdit: true,
+ suggestedColors: mockSuggestedColors,
+ emptyValueText: 'None',
+};
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 862b1fe3fd3..0c524a1551f 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -381,11 +381,11 @@ describe Banzai::Filter::LabelReferenceFilter do
end
it 'has valid link text' do
- expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}"
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project2.full_name}"
end
it 'has valid text' do
- expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}"
+ expect(result.text).to eq "See #{label.name} in #{project2.full_name}"
end
it 'ignores invalid IDs on the referenced label' do
@@ -481,12 +481,12 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'has valid link text' do
expect(result.css('a').first.text)
- .to eq "#{group_label.name} in #{another_project.name_with_namespace}"
+ .to eq "#{group_label.name} in #{another_project.full_name}"
end
it 'has valid text' do
expect(result.text)
- .to eq "See #{group_label.name} in #{another_project.name_with_namespace}"
+ .to eq "See #{group_label.name} in #{another_project.full_name}"
end
it 'ignores invalid IDs on the referenced label' do
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 91c43f2bdc0..ee91decafad 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::DataBuilder::Build do
it { expect(data[:build_status]).to eq(build.status) }
it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:project_id]).to eq(build.project.id) }
- it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
+ it { expect(data[:project_name]).to eq(build.project.full_name) }
context 'commit author_url' do
context 'when no commit present' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 41a55027f4d..b20cc34dd5c 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -277,6 +277,7 @@ project:
- fork_network
- custom_attributes
- lfs_file_locks
+- project_badges
award_emoji:
- awardable
- user
@@ -293,3 +294,5 @@ issue_assignees:
- assignee
lfs_file_locks:
- user
+project_badges:
+- project
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index b6c1f0c81cb..62ef93f847a 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -14,8 +14,7 @@
"template": false,
"description": "",
"type": "ProjectLabel",
- "priorities": [
- ]
+ "priorities": []
},
{
"id": 3,
@@ -160,9 +159,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 352,
@@ -184,9 +181,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 353,
@@ -208,9 +203,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 354,
@@ -232,9 +225,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 355,
@@ -256,9 +247,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 356,
@@ -280,9 +269,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 357,
@@ -304,9 +291,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 358,
@@ -328,9 +313,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -395,9 +378,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 360,
@@ -419,9 +400,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 361,
@@ -443,9 +422,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 362,
@@ -467,9 +444,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 363,
@@ -491,9 +466,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 364,
@@ -515,9 +488,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 365,
@@ -539,9 +510,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 366,
@@ -563,9 +532,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -628,9 +595,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 368,
@@ -652,9 +617,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 369,
@@ -676,9 +639,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 370,
@@ -700,9 +661,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 371,
@@ -724,9 +683,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 372,
@@ -748,9 +705,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 373,
@@ -772,9 +727,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 374,
@@ -796,9 +749,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -840,9 +791,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 376,
@@ -864,9 +813,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 377,
@@ -888,9 +835,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 378,
@@ -912,9 +857,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 379,
@@ -936,9 +879,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 380,
@@ -960,9 +901,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 381,
@@ -984,9 +923,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 382,
@@ -1008,9 +945,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1052,9 +987,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 384,
@@ -1076,9 +1009,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 385,
@@ -1100,9 +1031,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 386,
@@ -1124,9 +1053,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 387,
@@ -1148,9 +1075,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 388,
@@ -1172,9 +1097,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 389,
@@ -1196,9 +1119,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 390,
@@ -1220,9 +1141,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1264,9 +1183,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 392,
@@ -1288,9 +1205,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 393,
@@ -1312,9 +1227,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 394,
@@ -1336,9 +1249,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 395,
@@ -1360,9 +1271,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 396,
@@ -1384,9 +1293,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 397,
@@ -1408,9 +1315,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 398,
@@ -1432,9 +1337,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1476,9 +1379,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 400,
@@ -1500,9 +1401,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 401,
@@ -1524,9 +1423,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 402,
@@ -1548,9 +1445,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 403,
@@ -1572,9 +1467,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 404,
@@ -1596,9 +1489,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 405,
@@ -1620,9 +1511,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 406,
@@ -1644,9 +1533,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1688,9 +1575,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 408,
@@ -1712,9 +1597,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 409,
@@ -1736,9 +1619,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 410,
@@ -1760,9 +1641,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 411,
@@ -1784,9 +1663,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 412,
@@ -1808,9 +1685,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 413,
@@ -1832,9 +1707,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 414,
@@ -1856,9 +1729,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1900,9 +1771,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 416,
@@ -1924,9 +1793,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 417,
@@ -1948,9 +1815,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 418,
@@ -1972,9 +1837,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 419,
@@ -1996,9 +1859,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 420,
@@ -2020,9 +1881,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 421,
@@ -2044,9 +1903,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 422,
@@ -2068,9 +1925,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -2112,9 +1967,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 424,
@@ -2136,9 +1989,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 425,
@@ -2160,9 +2011,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 426,
@@ -2184,9 +2033,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 427,
@@ -2208,9 +2055,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 428,
@@ -2232,9 +2077,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 429,
@@ -2256,9 +2099,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 430,
@@ -2280,9 +2121,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
}
@@ -2378,12 +2217,8 @@
]
}
],
- "snippets": [
-
- ],
- "releases": [
-
- ],
+ "snippets": [],
+ "releases": [],
"project_members": [
{
"id": 36,
@@ -2515,9 +2350,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 672,
@@ -2539,9 +2372,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 673,
@@ -2563,9 +2394,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 674,
@@ -2587,9 +2416,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 675,
@@ -2611,9 +2438,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 676,
@@ -2635,9 +2460,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 677,
@@ -2659,9 +2482,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 678,
@@ -2683,9 +2504,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -2696,7 +2515,7 @@
"merge_request_diff_id": 27,
"relative_order": 0,
"sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
- "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-08-06T08:35:52.000+02:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2708,7 +2527,7 @@
"merge_request_diff_id": 27,
"relative_order": 1,
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2720,7 +2539,7 @@
"merge_request_diff_id": 27,
"relative_order": 2,
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2732,7 +2551,7 @@
"merge_request_diff_id": 27,
"relative_order": 3,
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
- "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2744,7 +2563,7 @@
"merge_request_diff_id": 27,
"relative_order": 4,
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
- "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2756,7 +2575,7 @@
"merge_request_diff_id": 27,
"relative_order": 5,
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
- "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2834,7 +2653,7 @@
{
"merge_request_diff_id": 27,
"relative_order": 5,
- "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -2958,9 +2777,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 680,
@@ -2982,9 +2799,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 681,
@@ -3006,9 +2821,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 682,
@@ -3030,9 +2843,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 683,
@@ -3054,9 +2865,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 684,
@@ -3078,9 +2887,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 685,
@@ -3102,9 +2909,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 686,
@@ -3126,9 +2931,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -3139,7 +2942,7 @@
"merge_request_diff_id": 26,
"sha": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
"relative_order": 0,
- "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:26:01.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3237,9 +3040,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 778,
@@ -3261,9 +3062,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 779,
@@ -3285,9 +3084,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 780,
@@ -3309,9 +3106,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 781,
@@ -3333,9 +3128,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 782,
@@ -3357,9 +3150,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 783,
@@ -3381,9 +3172,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 784,
@@ -3405,9 +3194,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -3516,9 +3303,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 786,
@@ -3540,9 +3325,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 787,
@@ -3564,9 +3347,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 788,
@@ -3588,9 +3369,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 789,
@@ -3612,9 +3391,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 790,
@@ -3636,9 +3413,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 791,
@@ -3660,9 +3435,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 792,
@@ -3684,9 +3457,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -3877,7 +3648,7 @@
"merge_request_diff_id": 14,
"relative_order": 15,
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3889,7 +3660,7 @@
"merge_request_diff_id": 14,
"relative_order": 16,
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3901,7 +3672,7 @@
"merge_request_diff_id": 14,
"relative_order": 17,
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
- "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3913,7 +3684,7 @@
"merge_request_diff_id": 14,
"relative_order": 18,
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
- "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3925,7 +3696,7 @@
"merge_request_diff_id": 14,
"relative_order": 19,
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
- "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -4016,7 +3787,7 @@
{
"merge_request_diff_id": 14,
"relative_order": 6,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4042,7 +3813,7 @@
{
"merge_request_diff_id": 14,
"relative_order": 8,
- "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -4207,7 +3978,7 @@
},
"events": [
{
- "merge_request_diff_id": 14,
+ "merge_request_diff_id": 14,
"id": 529,
"target_type": "Note",
"target_id": 793,
@@ -4239,9 +4010,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 795,
@@ -4263,9 +4032,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 796,
@@ -4287,9 +4054,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 797,
@@ -4311,9 +4076,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 798,
@@ -4335,9 +4098,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 799,
@@ -4359,9 +4120,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 800,
@@ -4383,9 +4142,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -4603,7 +4360,7 @@
{
"merge_request_diff_id": 13,
"relative_order": 2,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4740,9 +4497,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 802,
@@ -4764,9 +4519,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 803,
@@ -4788,9 +4541,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 804,
@@ -4812,9 +4563,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 805,
@@ -4836,9 +4585,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 806,
@@ -4860,9 +4607,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 807,
@@ -4884,9 +4629,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 808,
@@ -4908,9 +4651,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -5104,7 +4845,7 @@
{
"merge_request_diff_id": 12,
"relative_order": 2,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -5228,9 +4969,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 810,
@@ -5252,9 +4991,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 811,
@@ -5276,9 +5013,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 812,
@@ -5300,9 +5035,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 813,
@@ -5324,9 +5057,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 814,
@@ -5348,9 +5079,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 815,
@@ -5372,9 +5101,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 816,
@@ -5396,18 +5123,14 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
"id": 11,
"state": "empty",
- "merge_request_diff_commits": [
- ],
- "merge_request_diff_files": [
- ],
+ "merge_request_diff_commits": [],
+ "merge_request_diff_files": [],
"merge_request_id": 11,
"created_at": "2016-06-14T15:02:23.772Z",
"updated_at": "2016-06-14T15:02:23.833Z",
@@ -5482,9 +5205,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 818,
@@ -5506,9 +5227,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 819,
@@ -5530,9 +5249,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 820,
@@ -5554,9 +5271,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 821,
@@ -5578,9 +5293,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 822,
@@ -5602,9 +5315,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 823,
@@ -5626,9 +5337,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 824,
@@ -5650,9 +5359,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -5843,7 +5550,7 @@
"merge_request_diff_id": 10,
"relative_order": 16,
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5855,7 +5562,7 @@
"merge_request_diff_id": 10,
"relative_order": 17,
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5867,7 +5574,7 @@
"merge_request_diff_id": 10,
"relative_order": 18,
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
- "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5879,7 +5586,7 @@
"merge_request_diff_id": 10,
"relative_order": 19,
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
- "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5891,7 +5598,7 @@
"merge_request_diff_id": 10,
"relative_order": 20,
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
- "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5982,7 +5689,7 @@
{
"merge_request_diff_id": 10,
"relative_order": 6,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -6008,7 +5715,7 @@
{
"merge_request_diff_id": 10,
"relative_order": 8,
- "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -6171,9 +5878,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 826,
@@ -6195,9 +5900,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 827,
@@ -6219,9 +5922,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 828,
@@ -6243,9 +5944,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 829,
@@ -6267,9 +5966,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 830,
@@ -6291,9 +5988,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 831,
@@ -6315,9 +6010,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 832,
@@ -6339,9 +6032,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -6953,9 +6644,7 @@
"updated_at": "2017-01-16T15:25:28.637Z"
}
],
- "deploy_keys": [
-
- ],
+ "deploy_keys": [],
"services": [
{
"id": 100,
@@ -6964,9 +6653,7 @@
"created_at": "2016-06-14T15:01:51.315Z",
"updated_at": "2016-06-14T15:01:51.315Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7008,9 +6695,7 @@
"created_at": "2016-06-14T15:01:51.289Z",
"updated_at": "2016-06-14T15:01:51.289Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7030,9 +6715,7 @@
"created_at": "2016-06-14T15:01:51.277Z",
"updated_at": "2016-06-14T15:01:51.277Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7052,9 +6735,7 @@
"created_at": "2016-06-14T15:01:51.267Z",
"updated_at": "2016-06-14T15:01:51.267Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7097,9 +6778,7 @@
"created_at": "2016-06-14T15:01:51.232Z",
"updated_at": "2016-06-14T15:01:51.232Z",
"active": true,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7141,9 +6820,7 @@
"created_at": "2016-06-14T15:01:51.202Z",
"updated_at": "2016-06-14T15:01:51.202Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7163,9 +6840,7 @@
"created_at": "2016-06-14T15:01:51.182Z",
"updated_at": "2016-06-14T15:01:51.182Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7185,9 +6860,7 @@
"created_at": "2016-06-14T15:01:51.166Z",
"updated_at": "2016-06-14T15:01:51.166Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7207,9 +6880,7 @@
"created_at": "2016-06-14T15:01:51.153Z",
"updated_at": "2016-06-14T15:01:51.153Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7229,9 +6900,7 @@
"created_at": "2016-06-14T15:01:51.139Z",
"updated_at": "2016-06-14T15:01:51.139Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7251,9 +6920,7 @@
"created_at": "2016-06-14T15:01:51.125Z",
"updated_at": "2016-06-14T15:01:51.125Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7273,9 +6940,7 @@
"created_at": "2016-06-14T15:01:51.113Z",
"updated_at": "2016-06-14T15:01:51.113Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7295,9 +6960,7 @@
"created_at": "2016-06-14T15:01:51.080Z",
"updated_at": "2016-06-14T15:01:51.080Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7317,9 +6980,7 @@
"created_at": "2016-06-14T15:01:51.067Z",
"updated_at": "2016-06-14T15:01:51.067Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7339,9 +7000,7 @@
"created_at": "2016-06-14T15:01:51.047Z",
"updated_at": "2016-06-14T15:01:51.047Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7361,9 +7020,7 @@
"created_at": "2016-06-14T15:01:51.031Z",
"updated_at": "2016-06-14T15:01:51.031Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7383,9 +7040,7 @@
"created_at": "2016-06-14T15:01:51.031Z",
"updated_at": "2016-06-14T15:01:51.031Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7399,9 +7054,7 @@
"type": "JenkinsDeprecatedService"
}
],
- "hooks": [
-
- ],
+ "hooks": [],
"protected_branches": [
{
"id": 1,
@@ -7475,5 +7128,25 @@
"key": "bar",
"value": "bar"
}
+ ],
+ "project_badges": [
+ {
+ "id": 1,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "project_id": 5,
+ "type": "ProjectBadge",
+ "link_url": "http://www.example.com",
+ "image_url": "http://www.example.com"
+ },
+ {
+ "id": 2,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "project_id": 5,
+ "type": "ProjectBadge",
+ "link_url": "http://www.example.com",
+ "image_url": "http://www.example.com"
+ }
]
}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index d076007e4bc..1a4d09724fc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(@project.custom_attributes.count).to eq(2)
end
+ it 'has badges' do
+ expect(@project.project_badges.count).to eq(2)
+ end
+
it 'restores the correct service' do
expect(CustomIssueTrackerService.first).not_to be_nil
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 5804c45871e..d6bd5f5c81d 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -180,6 +180,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['custom_attributes'].count).to eq(2)
end
+ it 'has badges' do
+ expect(saved_project_json['project_badges'].count).to eq(2)
+ end
+
it 'does not complain about non UTF-8 characters in MR diff files' do
ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
@@ -288,6 +292,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:project_custom_attribute, project: project)
create(:project_custom_attribute, project: project)
+ create(:project_badge, project: project)
+ create(:project_badge, project: project)
+
project
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index feaab6673cd..ddcbb7a0033 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -536,3 +536,12 @@ LfsFileLock:
- user_id
- project_id
- created_at
+Badge:
+- id
+- link_url
+- image_url
+- project_id
+- group_id
+- created_at
+- updated_at
+- type
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 07ba11b93a3..39ec2f37a83 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -11,15 +11,17 @@ describe Gitlab::Middleware::ReadOnly do
RSpec::Matchers.define :disallow_request do
match do |middleware|
- flash = middleware.send(:rack_flash)
- flash['alert'] && flash['alert'].include?('You cannot do writing operations')
+ alert = middleware.env['rack.session'].to_hash
+ .dig('flash', 'flashes', 'alert')
+
+ alert&.include?('You cannot perform write operations')
end
end
RSpec::Matchers.define :disallow_request_in_json do
match do |response|
json_response = JSON.parse(response.body)
- response.body.include?('You cannot do writing operations') && json_response.key?('message')
+ response.body.include?('You cannot perform write operations') && json_response.key?('message')
end
end
@@ -34,10 +36,25 @@ describe Gitlab::Middleware::ReadOnly do
rack.to_app
end
- subject { described_class.new(fake_app) }
+ let(:observe_env) do
+ Module.new do
+ attr_reader :env
+
+ def call(env)
+ @env = env
+ super
+ end
+ end
+ end
let(:request) { Rack::MockRequest.new(rack_stack) }
+ subject do
+ described_class.new(fake_app).tap do |app|
+ app.extend(observe_env)
+ end
+ end
+
context 'normal requests to a read-only Gitlab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb
new file mode 100644
index 00000000000..5e3aa877409
--- /dev/null
+++ b/spec/lib/gitlab/middleware/release_env_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::ReleaseEnv do
+ let(:inner_app) { double(:app, call: 'yay') }
+ let(:app) { described_class.new(inner_app) }
+ let(:env) { { 'action_controller.instance' => 'something' } }
+
+ describe '#call' do
+ it 'calls the app and clears the env' do
+ result = app.call(env)
+
+ expect(result).to eq('yay')
+ expect(env).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb
new file mode 100644
index 00000000000..7a03ea4154c
--- /dev/null
+++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::StringPlaceholderReplacer do
+ describe '.render_url' do
+ it 'returns the nil if the string is blank' do
+ expect(described_class.replace_string_placeholders(nil, /whatever/)).to be_blank
+ end
+
+ it 'returns the string if the placeholder regex' do
+ expect(described_class.replace_string_placeholders('whatever')).to eq 'whatever'
+ end
+
+ it 'returns the string if no block given' do
+ expect(described_class.replace_string_placeholders('whatever', /whatever/)).to eq 'whatever'
+ end
+
+ context 'when all params are valid' do
+ let(:string) { '%{path}/%{id}/%{branch}' }
+ let(:regex) { /(path|id)/ }
+
+ it 'replaces each placeholders with the block result' do
+ result = described_class.replace_string_placeholders(string, regex) do |arg|
+ 'WHATEVER'
+ end
+
+ expect(result).to eq 'WHATEVER/WHATEVER/%{branch}'
+ end
+
+ it 'does not replace the placeholder if the block result is nil' do
+ result = described_class.replace_string_placeholders(string, regex) do |arg|
+ arg == 'path' ? nil : 'WHATEVER'
+ end
+
+ expect(result).to eq '%{path}/WHATEVER/%{branch}'
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index bcbb9287199..83c33797bbc 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -457,7 +457,7 @@ describe Notify do
it 'has the correct subject and body' do
is_expected.to have_subject("#{project.name} | Project was moved")
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text(project.ssh_url_to_repo)
end
end
@@ -483,8 +483,8 @@ describe Notify do
to_emails = subject.header[:to].addrs.map(&:address)
expect(to_emails).to eq([recipient.notification_email])
- is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Request to join the #{project.full_name} project"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project_project_members_url(project)
is_expected.to have_body_text project_member.human_access
end
@@ -503,8 +503,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
- is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Access to the #{project.full_name} project was denied"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
end
end
@@ -520,8 +520,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
- is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Access to the #{project.full_name} project was granted"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
end
@@ -550,8 +550,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
- is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Invitation to join the #{project.full_name} project"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
@@ -575,7 +575,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
is_expected.to have_html_escaped_body_text invited_user.name
@@ -598,7 +598,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
end
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
new file mode 100644
index 00000000000..33dc19e3432
--- /dev/null
+++ b/spec/models/badge_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Badge do
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+
+ describe 'validations' do
+ # Requires the let variable url_sym
+ shared_examples 'placeholder url' do
+ let(:badge) { build(:badge) }
+
+ it 'allows url with http protocol' do
+ badge[url_sym] = 'http://www.example.com'
+
+ expect(badge).to be_valid
+ end
+
+ it 'allows url with https protocol' do
+ badge[url_sym] = 'https://www.example.com'
+
+ expect(badge).to be_valid
+ end
+
+ it 'cannot be empty' do
+ badge[url_sym] = ''
+
+ expect(badge).not_to be_valid
+ end
+
+ it 'cannot be nil' do
+ badge[url_sym] = nil
+
+ expect(badge).not_to be_valid
+ end
+
+ it 'accept badges placeholders' do
+ badge[url_sym] = placeholder_url
+
+ expect(badge).to be_valid
+ end
+
+ it 'sanitize url' do
+ badge[url_sym] = 'javascript:alert(1)'
+
+ expect(badge).not_to be_valid
+ end
+ end
+
+ context 'link_url format' do
+ let(:url_sym) { :link_url }
+
+ it_behaves_like 'placeholder url'
+ end
+
+ context 'image_url format' do
+ let(:url_sym) { :image_url }
+
+ it_behaves_like 'placeholder url'
+ end
+ end
+
+ shared_examples 'rendered_links' do
+ it 'should use the project information to populate the url placeholders' do
+ stub_project_commit_info(project)
+
+ expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
+ end
+
+ it 'returns the url if the project used is nil' do
+ expect(badge.public_send("rendered_#{method}", nil)).to eq placeholder_url
+ end
+
+ def stub_project_commit_info(project)
+ allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever'))
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+ end
+
+ context 'methods' do
+ let(:badge) { build(:badge, link_url: placeholder_url, image_url: placeholder_url) }
+ let!(:project) { create(:project) }
+
+ context '#rendered_link_url' do
+ let(:method) { :link_url }
+
+ it_behaves_like 'rendered_links'
+ end
+
+ context '#rendered_image_url' do
+ let(:method) { :image_url }
+
+ it_behaves_like 'rendered_links'
+ end
+ end
+end
diff --git a/spec/models/badges/group_badge_spec.rb b/spec/models/badges/group_badge_spec.rb
new file mode 100644
index 00000000000..ed7f83d0489
--- /dev/null
+++ b/spec/models/badges/group_badge_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe GroupBadge do
+ describe 'associations' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:group) }
+ end
+end
diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb
new file mode 100644
index 00000000000..0e1a8159cb6
--- /dev/null
+++ b/spec/models/badges/project_badge_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe ProjectBadge do
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+
+ shared_examples 'rendered_links' do
+ it 'should use the badge project information to populate the url placeholders' do
+ stub_project_commit_info(project)
+
+ expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
+ end
+
+ def stub_project_commit_info(project)
+ allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever'))
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+ end
+
+ context 'methods' do
+ let(:badge) { build(:project_badge, link_url: placeholder_url, image_url: placeholder_url) }
+ let!(:project) { badge.project }
+
+ context '#rendered_link_url' do
+ let(:method) { :link_url }
+
+ it_behaves_like 'rendered_links'
+ end
+
+ context '#rendered_image_url' do
+ let(:method) { :image_url }
+
+ it_behaves_like 'rendered_links'
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 612a3c8e413..a574779e39d 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -34,6 +34,8 @@ describe Clusters::Applications::Runner do
is_expected.to include('checkInterval')
is_expected.to include('rbac')
is_expected.to include('runners')
+ is_expected.to include('privileged: true')
+ is_expected.to include('image: ubuntu:16.04')
is_expected.to include('resources')
is_expected.to include("runnerToken: #{ci_runner.token}")
is_expected.to include("gitlabUrl: #{Gitlab::Routing.url_helpers.root_url}")
@@ -65,5 +67,33 @@ describe Clusters::Applications::Runner do
expect(gitlab_runner.runner).not_to be_nil
end
end
+
+ context 'with duplicated values on vendor/runner/values.yaml' do
+ let(:values) do
+ {
+ "concurrent" => 4,
+ "checkInterval" => 3,
+ "rbac" => {
+ "create" => false
+ },
+ "clusterWideAccess" => false,
+ "runners" => {
+ "privileged" => false,
+ "image" => "ubuntu:16.04",
+ "builds" => {},
+ "services" => {},
+ "helpers" => {}
+ }
+ }
+ end
+
+ before do
+ allow(gitlab_runner).to receive(:chart_values).and_return(values)
+ end
+
+ it 'should overwrite values.yaml' do
+ is_expected.to include("privileged: #{gitlab_runner.privileged}")
+ end
+ end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4f16b73ef38..abfc0896a41 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -18,6 +18,7 @@ describe Group do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
+ it { is_expected.to have_many(:badges).class_name('GroupBadge') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 04440d890aa..e66109fd98f 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -47,7 +47,7 @@ describe AsanaService do
it 'calls Asana service to create a story' do
data = create_data_for_commits('Message from commit. related to #123456')
- expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
+ expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
d1 = double('Asana::Task')
expect(d1).to receive(:add_comment).with(text: expected_message)
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 23db29cb541..3e2a166cdd6 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -29,7 +29,7 @@ describe HipchatService do
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.name_with_namespace.gsub(/\s/, '') }
+ let(:project_name) { project.full_name.gsub(/\s/, '') }
let(:token) { 'verySecret' }
let(:server_url) { 'https://hipchat.example.com'}
let(:push_sample_data) do
@@ -303,7 +303,7 @@ describe HipchatService do
message = hipchat.__send__(:create_pipeline_message, data)
project_url = project.web_url
- project_name = project.name_with_namespace.gsub(/\s/, '')
+ project_name = project.full_name.gsub(/\s/, '')
pipeline_attributes = data[:object_attributes]
ref = pipeline_attributes[:ref]
ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 748c366efca..54ef0be67ff 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -166,7 +166,6 @@ describe JiraService do
# Creates comment
expect(WebMock).to have_requested(:post, @comment_url)
-
# Creates Remote Link in JIRA issue fields
expect(WebMock).to have_requested(:post, @remote_link_url).with(
body: hash_including(
@@ -174,7 +173,7 @@ describe JiraService do
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}",
title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: true }
}
)
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 522cf15f3ba..a5bdf9a9337 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -31,10 +31,10 @@ describe MattermostSlashCommandsService do
url: 'http://trigger.url',
icon_url: 'http://icon.url/icon.png',
auto_complete: true,
- auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}",
+ auto_complete_desc: "Perform common operations on: #{project.full_name}",
auto_complete_hint: '[help]',
- description: "Perform common operations on: #{project.name_with_namespace}",
- display_name: "GitLab / #{project.name_with_namespace}",
+ description: "Perform common operations on: #{project.full_name}",
+ display_name: "GitLab / #{project.full_name}",
method: 'P',
username: 'GitLab'
}.to_json)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f4faec9e52a..92ea8841123 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -80,6 +80,7 @@ describe Project do
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
+ it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
it { is_expected.to have_many(:lfs_file_locks) }
context 'after initialized' do
@@ -3331,4 +3332,36 @@ describe Project do
end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
end
end
+
+ describe '#badges' do
+ let(:project_group) { create(:group) }
+ let(:project) { create(:project, path: 'avatar', namespace: project_group) }
+
+ before do
+ create_list(:project_badge, 2, project: project)
+ create(:group_badge, group: project_group)
+ end
+
+ it 'returns the project and the project group badges' do
+ create(:group_badge, group: create(:group))
+
+ expect(Badge.count).to eq 4
+ expect(project.badges.count).to eq 3
+ end
+
+ if Group.supports_nested_groups?
+ context 'with nested_groups' do
+ let(:parent_group) { create(:group) }
+
+ before do
+ create_list(:group_badge, 2, group: project_group)
+ project_group.update(parent: parent_group)
+ end
+
+ it 'returns the project and the project nested groups badges' do
+ expect(project.badges.count).to eq 5
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 1e7671476f1..8b4b5873704 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -14,13 +14,13 @@ describe ProjectWiki do
it { is_expected.to delegate_method(:repository_storage_path).to :project }
it { is_expected.to delegate_method(:hashed_storage?).to :project }
- describe "#path_with_namespace" do
+ describe "#full_path" do
it "returns the project path with namespace with the .wiki extension" do
- expect(subject.path_with_namespace).to eq(project.full_path + '.wiki')
+ expect(subject.full_path).to eq(project.full_path + '.wiki')
end
it 'returns the same value as #full_path' do
- expect(subject.path_with_namespace).to eq(subject.full_path)
+ expect(subject.full_path).to eq(subject.full_path)
end
end
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
new file mode 100644
index 00000000000..ae64a9ca162
--- /dev/null
+++ b/spec/requests/api/badges_spec.rb
@@ -0,0 +1,367 @@
+require 'spec_helper'
+
+describe API::Badges do
+ let(:master) { create(:user, username: 'master_user') }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+ let(:project_group) { create(:group) }
+ let(:project) { setup_project }
+ let!(:group) { setup_group }
+
+ shared_context 'source helpers' do
+ def get_source(source_type)
+ source_type == 'project' ? project : group
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/badges' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges", stranger) }
+ end
+
+ %i[master developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ badges_count = source_type == 'project' ? 3 : 2
+
+ get api("/#{source_type.pluralize}/#{source.id}/badges", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(badges_count)
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ end
+
+ project.add_developer(create(:user))
+
+ expect do
+ get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/badges/:badge_id' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[master developer access_requester stranger].each do |type|
+ let(:badge) { source.badges.first }
+
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+
+ get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(badge.id)
+ expect(json_response['link_url']).to eq(badge.link_url)
+ expect(json_response['rendered_link_url']).to eq(badge.rendered_link_url)
+ expect(json_response['image_url']).to eq(badge.image_url)
+ expect(json_response['rendered_image_url']).to eq(badge.rendered_image_url)
+ expect(json_response['kind']).to eq source_type
+ end
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/badges' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+ let(:example_url) { 'http://www.example.com' }
+ let(:example_url2) { 'http://www.example1.com' }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", stranger),
+ link_url: example_url, image_url: example_url2
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ post api("/#{source_type.pluralize}/#{source.id}/badges", user),
+ link_url: example_url, image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'creates a new badge' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ link_url: example_url, image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(201)
+ end.to change { source.badges.count }.by(1)
+
+ expect(json_response['link_url']).to eq(example_url)
+ expect(json_response['image_url']).to eq(example_url2)
+ expect(json_response['kind']).to eq source_type
+ end
+ end
+
+ it 'returns 400 when link_url is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ link_url: example_url
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when image_url is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when link_url or image_url is not valid' do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ link_url: 'whatever', image_url: 'whatever'
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/badges/:badge_id' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ let(:badge) { source.badges.first }
+ let(:example_url) { 'http://www.example.com' }
+ let(:example_url2) { 'http://www.example1.com' }
+
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger),
+ link_url: example_url
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user),
+ link_url: example_url
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'updates the member' do
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master),
+ link_url: example_url, image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['link_url']).to eq(example_url)
+ expect(json_response['image_url']).to eq(example_url2)
+ expect(json_response['kind']).to eq source_type
+ end
+ end
+
+ it 'returns 400 when link_url or image_url is not valid' do
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master),
+ link_url: 'whatever', image_url: 'whatever'
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/badges/:badge_id' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ let(:badge) { source.badges.first }
+
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester developer stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'deletes the badge' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.to change { source.badges.count }.by(-1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) }
+ end
+ end
+
+ it 'returns 404 if badge does not exist' do
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/123", master)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/badges/render' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+ let(:example_url) { 'http://www.example.com' }
+ let(:example_url2) { 'http://www.example1.com' }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", stranger)
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'gets the rendered badge values' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", master)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response.keys).to contain_exactly('link_url', 'rendered_link_url', 'image_url', 'rendered_image_url')
+ expect(json_response['link_url']).to eq(example_url)
+ expect(json_response['image_url']).to eq(example_url2)
+ expect(json_response['rendered_link_url']).to eq(example_url)
+ expect(json_response['rendered_image_url']).to eq(example_url2)
+ end
+ end
+
+ it 'returns 400 when link_url is not given' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}", master)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when image_url is not given' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?image_url=#{example_url}", master)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when link_url or image_url is not valid' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=whatever&image_url=whatever", master)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ context 'when deleting a badge' do
+ context 'and the source is a project' do
+ it 'cannot delete badges owned by the project group' do
+ delete api("/projects/#{project.id}/badges/#{project_group.badges.first.id}", master)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ describe 'Endpoints' do
+ %w(project group).each do |source_type|
+ it_behaves_like 'GET /:sources/:id/badges', source_type
+ it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type
+ it_behaves_like 'GET /:sources/:id/badges/render', source_type
+ it_behaves_like 'POST /:sources/:id/badges', source_type
+ it_behaves_like 'PUT /:sources/:id/badges/:badge_id', source_type
+ it_behaves_like 'DELETE /:sources/:id/badges/:badge_id', source_type
+ end
+ end
+
+ def setup_project
+ create(:project, :public, :access_requestable, creator_id: master.id, namespace: project_group) do |project|
+ project.add_developer(developer)
+ project.add_master(master)
+ project.request_access(access_requester)
+ project.project_badges << build(:project_badge, project: project)
+ project.project_badges << build(:project_badge, project: project)
+ project_group.badges << build(:group_badge, group: group)
+ end
+ end
+
+ def setup_group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ group.badges << build(:group_badge, group: group)
+ group.badges << build(:group_badge, group: group)
+ end
+ end
+end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index 025165622b7..dc3a116c060 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -16,7 +16,7 @@ describe API::PagesDomains do
let(:route) { "/projects/#{project.id}/pages/domains" }
let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" }
- let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" }
+ let(:route_domain_path) { "/projects/#{project.full_path.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" }
let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" }
let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" }
let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 72cafac3f90..ce1311ac97c 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1100,11 +1100,13 @@ describe API::Runner do
context 'posts artifacts file and metadata file' do
let!(:artifacts) { file_upload }
+ let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest }
let!(:metadata) { file_upload2 }
let(:stored_artifacts_file) { job.reload.artifacts_file.file }
let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
let(:stored_artifacts_size) { job.reload.artifacts_size }
+ let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 }
before do
post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
@@ -1114,6 +1116,7 @@ describe API::Runner do
let(:post_data) do
{ 'file.path' => artifacts.path,
'file.name' => artifacts.original_filename,
+ 'file.sha256' => artifacts_sha256,
'metadata.path' => metadata.path,
'metadata.name' => metadata.original_filename }
end
@@ -1123,6 +1126,7 @@ describe API::Runner do
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
expect(stored_artifacts_size).to eq(72821)
+ expect(stored_artifacts_sha256).to eq(artifacts_sha256)
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 0467e0251b3..6dbbb1ad7bb 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -795,9 +795,9 @@ describe 'Git HTTP requests' do
let(:path) { 'doesnt/exist.git' }
before do
- allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
- allow(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil)
- allow(Gitlab::Auth::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_return(true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
end
it_behaves_like 'pulls require Basic HTTP Authentication'
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 9128280eb5a..290eeae828e 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -172,7 +172,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -200,7 +200,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -239,7 +239,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -270,7 +270,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to delete images' do
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -311,7 +311,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to delete images' do
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -323,7 +323,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to pull or push images' do
let(:current_user) { create(:user, external: true) }
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ { scope: "repository:#{project.full_path}:pull,push" }
end
it_behaves_like 'an inaccessible'
@@ -333,7 +333,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to delete images' do
let(:current_user) { create(:user, external: true) }
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -359,7 +359,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'allow to delete images' do
let(:current_params) do
- { scope: "repository:#{current_project.path_with_namespace}:*" }
+ { scope: "repository:#{current_project.full_path}:*" }
end
it_behaves_like 'a deletable' do
@@ -398,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow to delete images' do
let(:current_params) do
- { scope: "repository:#{current_project.path_with_namespace}:*" }
+ { scope: "repository:#{current_project.full_path}:*" }
end
it_behaves_like 'an inaccessible' do
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index c40cd5b7548..51396d34f8f 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -30,6 +30,7 @@ describe SystemHooksService do
:old_path_with_namespace
)
end
+
it do
project.old_path_with_namespace = 'transfered_from_path'
expect(event_data(project, :transfer)).to include(
@@ -45,18 +46,21 @@ describe SystemHooksService do
:owner_name, :owner_email
)
end
+
it do
expect(event_data(group, :destroy)).to include(
:event_name, :name, :created_at, :updated_at, :path, :group_id,
:owner_name, :owner_email
)
end
+
it do
expect(event_data(group_member, :create)).to include(
:event_name, :created_at, :updated_at, :group_name, :group_path,
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
+
it do
expect(event_data(group_member, :destroy)).to include(
:event_name, :created_at, :updated_at, :group_name, :group_path,
@@ -70,6 +74,14 @@ describe SystemHooksService do
expect(data[:project_visibility]).to eq('private')
end
+ it 'handles nil datetime columns' do
+ user.update_attributes(created_at: nil, updated_at: nil)
+ data = event_data(user, :destroy)
+
+ expect(data[:created_at]).to be(nil)
+ expect(data[:updated_at]).to be(nil)
+ end
+
context 'group_rename' do
it 'contains old and new path' do
allow(group).to receive(:path_was).and_return('old-path')
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5b5edc1aa0d..a3893188c6e 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -789,7 +789,7 @@ describe SystemNoteService do
object: {
url: project_commit_url(project, commit),
title: "GitLab: Mentioned on commit - #{commit.title}",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: false }
}
)
@@ -815,7 +815,7 @@ describe SystemNoteService do
object: {
url: project_issue_url(project, issue),
title: "GitLab: Mentioned on issue - #{issue.title}",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: false }
}
)
@@ -841,7 +841,7 @@ describe SystemNoteService do
object: {
url: project_snippet_url(project, snippet),
title: "GitLab: Mentioned on snippet - #{snippet.title}",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: false }
}
)
diff --git a/spec/validators/url_placeholder_validator_spec.rb b/spec/validators/url_placeholder_validator_spec.rb
new file mode 100644
index 00000000000..b76d8acdf88
--- /dev/null
+++ b/spec/validators/url_placeholder_validator_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe UrlPlaceholderValidator do
+ let(:validator) { described_class.new(attributes: [:link_url], **options) }
+ let!(:badge) { build(:badge) }
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ describe '#validates_each' do
+ context 'with no options' do
+ let(:options) { {} }
+
+ it 'allows http and https protocols by default' do
+ expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
+ end
+
+ it 'checks that the url structure is valid' do
+ badge.link_url = placeholder_url
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'with placeholder regex' do
+ let(:options) { { placeholder_regex: /(project_path|project_id|commit_sha|default_branch)/ } }
+
+ it 'checks that the url is valid and obviate placeholders that match regex' do
+ badge.link_url = placeholder_url
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
new file mode 100644
index 00000000000..763dff181d2
--- /dev/null
+++ b/spec/validators/url_validator_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe UrlValidator do
+ let(:validator) { described_class.new(attributes: [:link_url], **options) }
+ let!(:badge) { build(:badge) }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ describe '#validates_each' do
+ context 'with no options' do
+ let(:options) { {} }
+
+ it 'allows http and https protocols by default' do
+ expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
+ end
+
+ it 'checks that the url structure is valid' do
+ badge.link_url = 'http://www.google.es/%{whatever}'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'with protocols' do
+ let(:options) { { protocols: %w(http) } }
+
+ it 'allows urls with the defined protocols' do
+ badge.link_url = 'http://www.example.com'
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+
+ it 'add error if the url protocol does not match the selected ones' do
+ badge.link_url = 'https://www.example.com'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 62af946dcab..15fce65979b 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe 'projects/_home_panel' do
- let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :public, namespace: group) }
let(:notification_settings) do
user&.notification_settings_for(project)
@@ -35,4 +36,55 @@ describe 'projects/_home_panel' do
expect(rendered).not_to have_selector('.notification_dropdown')
end
end
+
+ context 'when project' do
+ let!(:user) { create(:user) }
+ let(:badges) { project.badges }
+
+ context 'has no badges' do
+ it 'should not render any badge' do
+ render
+
+ expect(rendered).to have_selector('.project-badges')
+ expect(rendered).not_to have_selector('.project-badges > a')
+ end
+ end
+
+ shared_examples 'show badges' do
+ it 'should render the all badges' do
+ render
+
+ expect(rendered).to have_selector('.project-badges a')
+
+ badges.each do |badge|
+ expect(rendered).to have_link(href: badge.rendered_link_url)
+ end
+ end
+ end
+
+ context 'only has group badges' do
+ before do
+ create(:group_badge, group: project.group)
+ end
+
+ it_behaves_like 'show badges'
+ end
+
+ context 'only has project badges' do
+ before do
+ create(:project_badge, project: project)
+ end
+
+ it_behaves_like 'show badges'
+ end
+
+ context 'has both group and project badges' do
+ before do
+ create(:project_badge, project: project)
+ create(:group_badge, group: project.group)
+ end
+
+ it_behaves_like 'show badges'
+ end
+ end
end
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index 68cfe9d5545..615462380e0 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::GithubImport::ObjectImporter do
importer_class = double(:importer_class)
importer_instance = double(:importer_instance)
representation = double(:representation)
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
expect(worker)
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 47297de738b..74539a7e493 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -195,6 +195,12 @@ describe GitGarbageCollectWorker do
expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
end
+
+ it 'cleans up repository after finishing' do
+ expect_any_instance_of(Project).to receive(:cleanup).and_call_original
+
+ subject.perform(project.id, 'gc', lease_key, lease_uuid)
+ end
end
context 'with bitmaps enabled' do
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
index 7c8c665a9b3..48e7eaf32fc 100644
--- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportDiffNoteWorker do
describe '#import' do
it 'imports a diff note' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
index 4116380ff4d..8cf6ac15919 100644
--- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportIssueWorker do
describe '#import' do
it 'imports an issue' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
index 0ca825a722b..677697c02df 100644
--- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportNoteWorker do
describe '#import' do
it 'imports a note' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
index d49f560af42..e287ddbe0d7 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportPullRequestWorker do
describe '#import' do
it 'imports a pull request' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index dcf5e4a0416..06093deb459 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index d4856090ed9..85cc1b6bb78 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 6ee7e76f676..e98d3ce7b8f 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/vendor/runner/values.yaml b/vendor/runner/values.yaml
index b7e2e24acaf..e5f95152ac7 100644
--- a/vendor/runner/values.yaml
+++ b/vendor/runner/values.yaml
@@ -15,10 +15,8 @@ rbac:
clusterWideAccess: false
## Configuration for the Pods that that the runner launches for each new job
-##
runners:
image: ubuntu:16.04
- privileged: false
builds: {}
services: {}
helpers: {}