summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml95
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js5
-rw-r--r--app/assets/javascripts/mr_notes/index.js2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue5
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue82
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js33
-rw-r--r--app/assets/javascripts/notes/index.js3
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js5
-rw-r--r--app/assets/javascripts/notes/stores/actions.js25
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/stylesheets/pages/issues.scss23
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss21
-rw-r--r--app/controllers/concerns/issuable_actions.rb33
-rw-r--r--app/controllers/concerns/notes_actions.rb17
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/finders/notes_finder.rb6
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/models/ci/job_artifact.rb12
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/note.rb9
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/user_preference.rb52
-rw-r--r--app/serializers/current_user_entity.rb8
-rw-r--r--app/serializers/merge_request_user_entity.rb2
-rw-r--r--app/serializers/user_preference_entity.rb10
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml7
-rw-r--r--app/views/projects/merge_requests/show.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml15
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml4
-rw-r--r--app/views/shared/runners/show.html.haml2
-rw-r--r--changelogs/unreleased/26723-discussion-filters.yml5
-rw-r--r--changelogs/unreleased/32959-update-todo-icon.yml5
-rw-r--r--changelogs/unreleased/42611-removed-branch-link.yml5
-rw-r--r--changelogs/unreleased/52059-filter-milestone-by-none-any.yml5
-rw-r--r--changelogs/unreleased/52840-fix-runners-details-page.yml5
-rw-r--r--changelogs/unreleased/add-new-kubernetes-spec-helpers.yml5
-rw-r--r--changelogs/unreleased/add-role-binding-to-kubeclient.yml5
-rw-r--r--changelogs/unreleased/lfs-project-attribute-alias.yml5
-rw-r--r--changelogs/unreleased/sh-pages-eof-error.yml5
-rw-r--r--changelogs/unreleased/support-license-management-and-performance.yml5
-rw-r--r--changelogs/unreleased/update-runner-chart-to-0-1-35.yml5
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--db/migrate/20180925200829_create_user_preferences.rb31
-rw-r--r--db/schema.rb11
-rw-r--r--doc/integration/saml.md17
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb7
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb4
-rw-r--r--lib/gitlab/database/migration_helpers.rb2
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb7
-rw-r--r--lib/gitlab/kubernetes/role_binding.rb47
-rw-r--r--lib/gitlab/setup_helper.rb5
-rw-r--r--locale/gitlab.pot18
-rwxr-xr-xscripts/review_apps/review-apps.sh37
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb7
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb8
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb31
-rw-r--r--spec/factories/ci/runners.rb10
-rw-r--r--spec/factories/user_preferences.rb12
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb10
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb18
-rw-r--r--spec/finders/notes_finder_spec.rb21
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js12
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js60
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js3
-rw-r--r--spec/javascripts/notes/mock_data.js15
-rw-r--r--spec/lib/gitaly/server_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/role_binding_spec.rb47
-rw-r--r--spec/models/ci/pipeline_spec.rb35
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/note_spec.rb24
-rw-r--r--spec/models/user_preference_spec.rb32
-rw-r--r--spec/models/user_spec.rb9
-rw-r--r--spec/services/ci/retry_build_service_spec.rb1
-rw-r--r--spec/support/helpers/ci_artifact_metadata_generator.rb48
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb23
-rw-r--r--spec/support/helpers/test_env.rb9
-rw-r--r--spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb54
-rw-r--r--spec/support/stored_repositories.rb4
-rw-r--r--spec/views/shared/runners/show.html.haml_spec.rb155
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb2
96 files changed, 1402 insertions, 99 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c652b6c75e2..ccc9e640970 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -139,7 +139,7 @@ stages:
- export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
- apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
- - chmod 755 $SCRIPT_NAME
+ - chmod 755 $(basename $SCRIPT_NAME)
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -723,7 +723,7 @@ gitlab:assets:compile:
- public/assets/
karma:
- <<: *dedicated-no-docs-and-no-qa-pull-cache-job
+ <<: *dedicated-no-docs-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
@@ -929,3 +929,94 @@ no_ee_check:
- scripts/no-ee-check
only:
- //@gitlab-org/gitlab-ce
+
+# GitLab Review apps
+review:
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: test
+ allow_failure: true
+ before_script:
+ - gem install gitlab --no-document
+ variables:
+ GIT_DEPTH: "1"
+ HOST_SUFFIX: "$CI_ENVIRONMENT_SLUG"
+ DOMAIN: "-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN"
+ GITLAB_HELM_CHART_REF: "master"
+ script:
+ - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
+ - export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
+ - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
+ - source ./scripts/review_apps/review-apps.sh
+ - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
+ - check_kube_domain
+ - download_gitlab_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - install_external_dns
+ - deploy
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://gitlab-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN
+ on_stop: stop_review
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ refs:
+ - master
+ - /(^docs[\/-].*|.*-docs$)/
+
+stop_review:
+ <<: *single-script-job
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: test
+ allow_failure: true
+ cache: {}
+ dependencies: []
+ variables:
+ SCRIPT_NAME: "review_apps/review-apps.sh"
+ script:
+ - source $(basename "${SCRIPT_NAME}")
+ - delete
+ - cleanup
+ when: manual
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ - master
+ - /(^docs[\/-].*|.*-docs$)/
+
+schedule:review_apps_cleanup:
+ <<: *dedicated-no-docs-pull-cache-job
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: build
+ allow_failure: true
+ cache: {}
+ dependencies: []
+ before_script:
+ - gem install gitlab --no-document
+ variables:
+ GIT_DEPTH: "1"
+ script:
+ - ruby -rrubygems scripts/review_apps/automated_cleanup.rb
+ environment:
+ name: review/auto-cleanup
+ action: stop
+ only:
+ refs:
+ - schedules@gitlab-org/gitlab-ce
+ - schedules@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ - master
+ - tags
+ - /(^docs[\/-].*|.*-docs$)/
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 64b54b171f7..69cf7fe1548 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -16,7 +16,6 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
-- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
@@ -26,7 +25,8 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
-- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
+- [ ] Add the ~"Merge into Security" label to all of the MRs.
+- [ ] Make sure all MRs have a link in the [links section](#links)
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 33e061fe7a0..bcc9c2840a7 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.125.1
+0.126.0
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index c4f0c41d3a8..b70125c80ca 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -68,6 +68,11 @@ export const conditions = [
value: 'none',
},
{
+ url: 'milestone_title=Any+Milestone',
+ tokenKey: 'milestone',
+ value: 'any',
+ },
+ {
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
value: 'upcoming',
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 8aabb840847..1c98683c597 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -4,6 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
+import initDiscussionFilters from '../notes/discussion_filters';
import store from './stores';
import MergeRequest from '../merge_request';
@@ -88,5 +89,6 @@ export default function initMrNotes() {
},
});
+ initDiscussionFilters(store);
initDiffsApp(store);
}
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index ad6e7cf501d..1f80f24e045 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -56,10 +56,11 @@ export default {
</script>
<template>
- <div class="line-resolve-all-container prepend-top-10">
+ <div
+ v-if="discussionCount > 0"
+ class="line-resolve-all-container prepend-top-8">
<div>
<div
- v-if="discussionCount > 0"
:class="{ 'has-next-btn': hasNextButton }"
class="line-resolve-all">
<span
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
new file mode 100644
index 00000000000..27972682ca1
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -0,0 +1,82 @@
+<script>
+import $ from 'jquery';
+import Icon from '~/vue_shared/components/icon.vue';
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ filters: {
+ type: Array,
+ required: true,
+ },
+ defaultValue: {
+ type: Number,
+ default: null,
+ required: false,
+ },
+ },
+ data() {
+ return { currentValue: this.defaultValue };
+ },
+ computed: {
+ ...mapGetters([
+ 'getNotesDataByProp',
+ ]),
+ currentFilter() {
+ if (!this.currentValue) return this.filters[0];
+ return this.filters.find(filter => filter.value === this.currentValue);
+ },
+ },
+ methods: {
+ ...mapActions(['filterDiscussion']),
+ selectFilter(value) {
+ const filter = parseInt(value, 10);
+
+ // close dropdown
+ $(this.$refs.dropdownToggle).dropdown('toggle');
+
+ if (filter === this.currentValue) return;
+ this.currentValue = filter;
+ this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-filter-container d-inline-block align-bottom">
+ <button
+ id="discussion-filter-dropdown"
+ ref="dropdownToggle"
+ class="btn btn-default"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ {{ currentFilter.title }}
+ <icon name="chevron-down" />
+ </button>
+ <div
+ class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
+ aria-labelledby="discussion-filter-dropdown">
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="filter in filters"
+ :key="filter.value"
+ >
+ <button
+ :class="{ 'is-active': filter.value === currentValue }"
+ type="button"
+ @click="selectFilter(filter.value)"
+ >
+ {{ filter.title }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 618a1581d8f..b0faa443a18 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,11 +50,11 @@ export default {
},
data() {
return {
- isLoading: true,
+ currentFilter: null,
};
},
computed: {
- ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']),
+ ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']),
noteableType() {
return this.noteableData.noteableType;
},
@@ -102,6 +102,7 @@ export default {
},
methods: {
...mapActions({
+ setLoadingState: 'setLoadingState',
fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
@@ -133,19 +134,19 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
- return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath'))
+ return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
})
.then(() => {
- this.isLoading = false;
+ this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
- this.isLoading = false;
+ this.setLoadingState(false);
this.setNotesFetchedState(true);
Flash('Something went wrong while fetching comments. Please try again.');
});
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
new file mode 100644
index 00000000000..012ffc4093e
--- /dev/null
+++ b/app/assets/javascripts/notes/discussion_filters.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import DiscussionFilter from './components/discussion_filter.vue';
+
+export default (store) => {
+ const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
+
+ if (discussionFilterEl) {
+ const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
+ const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
+ const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
+ const filters = Object.keys(filterValues).map(entry =>
+ ({ title: entry, value: filterValues[entry] }));
+
+ return new Vue({
+ el: discussionFilterEl,
+ name: 'DiscussionFilter',
+ components: {
+ DiscussionFilter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-filter', {
+ props: {
+ filters,
+ defaultValue,
+ },
+ });
+ },
+ });
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 3aef30c608c..2f715c85fa6 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,10 +1,13 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
+import initDiscussionFilters from './discussion_filters';
import createStore from './stores';
document.addEventListener('DOMContentLoaded', () => {
const store = createStore();
+ initDiscussionFilters(store);
+
return new Vue({
el: '#js-vue-notes',
components: {
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index f5dce94caad..47a6f07cce2 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -5,8 +5,9 @@ import * as constants from '../constants';
Vue.use(VueResource);
export default {
- fetchDiscussions(endpoint) {
- return Vue.http.get(endpoint);
+ fetchDiscussions(endpoint, filter) {
+ const config = filter !== undefined ? { params: { notes_filter: filter } } : null;
+ return Vue.http.get(endpoint, config);
},
deleteNote(endpoint) {
return Vue.http.delete(endpoint);
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7ab7e5a9abb..b5dd49bc6c9 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -11,6 +11,7 @@ import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
+import { __ } from '~/locale';
let eTagPoll;
@@ -36,9 +37,9 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchDiscussions = ({ commit }, path) =>
+export const fetchDiscussions = ({ commit }, { path, filter }) =>
service
- .fetchDiscussions(path)
+ .fetchDiscussions(path, filter)
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
@@ -251,7 +252,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
- dispatch('fetchDiscussions', state.notesData.discussionsPath);
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -345,5 +346,23 @@ export const updateMergeRequestWidget = () => {
mrWidgetEventHub.$emit('mr.discussion.updated');
};
+export const setLoadingState = ({ commit }, data) => {
+ commit(types.SET_NOTES_LOADING_STATE, data);
+};
+
+export const filterDiscussion = ({ dispatch }, { path, filter }) => {
+ dispatch('setLoadingState', true);
+ dispatch('fetchDiscussions', { path, filter })
+ .then(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ })
+ .catch(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ Flash(__('Something went wrong while fetching comments. Please try again.'));
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index a829149a17e..21c334a9d33 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -11,6 +11,8 @@ export const getNotesData = state => state.notesData;
export const isNotesFetched = state => state.isNotesFetched;
+export const isLoading = state => state.isLoading;
+
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 61dbb075586..400142668ea 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -11,6 +11,7 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
isNotesFetched: false,
+ isLoading: true,
// holds endpoints and permissions provided through haml
notesData: {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 6f374f78691..2fa53aef1d4 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
+export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 73e55705f39..65085452139 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -216,6 +216,10 @@ export default {
Object.assign(state, { isNotesFetched: value });
},
+ [types.SET_NOTES_LOADING_STATE](state, value) {
+ state.isLoading = value;
+ },
+
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 0f95fb911e1..8ea34f5d19d 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -185,7 +185,17 @@ ul.related-merge-requests > li {
}
.new-branch-col {
- padding-top: 10px;
+ font-size: 0;
+
+ .discussion-filter-container {
+ &:not(:only-child) {
+ margin-right: $gl-padding-8;
+ }
+
+ @include media-breakpoint-down(md) {
+ margin-top: $gl-padding-8;
+ }
+ }
}
.create-mr-dropdown-wrap {
@@ -205,6 +215,10 @@ ul.related-merge-requests > li {
.btn-group:not(.hidden) {
display: flex;
+
+ @include media-breakpoint-down(md) {
+ margin-top: $gl-padding-8;
+ }
}
.js-create-merge-request {
@@ -251,7 +265,6 @@ ul.related-merge-requests > li {
.new-branch-col {
padding-top: 0;
- text-align: right;
align-self: center;
}
@@ -262,3 +275,9 @@ ul.related-merge-requests > li {
}
}
}
+
+@include media-breakpoint-up(lg) {
+ .new-branch-col {
+ text-align: right;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2feb7464ecb..fa6afbf81de 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -818,9 +818,17 @@
display: flex;
justify-content: space-between;
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(md) {
flex-direction: column-reverse;
}
+
+ .discussion-filter-container {
+ margin-top: $gl-padding-8;
+
+ &:not(:only-child) {
+ padding-right: $gl-padding-8;
+ }
+ }
}
.limit-container-width:not(.container-limited) {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index bfba1bf1b2b..be535ade0a6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -618,7 +618,6 @@ ul.notes {
.line-resolve-all-container {
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-right: 0;
- padding-left: $gl-padding;
}
> div {
@@ -756,3 +755,23 @@ ul.notes {
margin-top: 4px;
}
}
+
+.discussion-filter-container {
+
+ .btn > svg {
+ width: $gl-col-padding;
+ height: $gl-col-padding;
+ }
+
+ .dropdown-menu {
+ margin-bottom: $gl-padding-4;
+
+ @include media-breakpoint-down(md) {
+ margin-left: $btn-side-margin + $contextual-sidebar-collapsed-width;
+ }
+
+ @include media-breakpoint-down(xs) {
+ margin-left: $btn-side-margin;
+ }
+ }
+}
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 07e01e903ea..ad9cc0925b7 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -2,6 +2,7 @@
module IssuableActions
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
included do
before_action :labels, only: [:show, :new, :edit]
@@ -95,10 +96,14 @@ module IssuableActions
def discussions
notes = issuable.discussion_notes
.inc_relations_for_view
+ .with_notes_filter(notes_filter)
.includes(:noteable)
.fresh
- notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
+ end
+
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@@ -110,6 +115,32 @@ module IssuableActions
private
+ def notes_filter
+ strong_memoize(:notes_filter) do
+ notes_filter_param = params[:notes_filter]&.to_i
+
+ # GitLab Geo does not expect database UPDATE or INSERT statements to happen
+ # on GET requests.
+ # This is just a fail-safe in case notes_filter is sent via GET request in GitLab Geo.
+ if Gitlab::Database.read_only?
+ notes_filter_param || current_user&.notes_filter_for(issuable)
+ else
+ notes_filter = current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
+
+ # We need to invalidate the cache for polling notes otherwise it will
+ # ignore the filter.
+ # The ideal would be to invalidate the cache for each user.
+ issuable.expire_note_etag_cache if notes_filter_updated?
+
+ notes_filter
+ end
+ end
+ end
+
+ def notes_filter_updated?
+ current_user&.user_preference&.previous_changes&.any?
+ end
+
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 3a45d6205ab..777b147e2dd 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -17,10 +17,17 @@ module NotesActions
notes_json = { notes: [], last_fetched_at: current_fetched_at }
- notes = notes_finder.execute
- .inc_relations_for_view
+ notes = notes_finder
+ .execute
+ .inc_relations_for_view
+
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ notes =
+ ResourceEvents::MergeIntoNotesService
+ .new(noteable, current_user, last_fetched_at: current_fetched_at)
+ .execute(notes)
+ end
- notes = ResourceEvents::MergeIntoNotesService.new(noteable, current_user, last_fetched_at: current_fetched_at).execute(notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@@ -224,6 +231,10 @@ module NotesActions
request.headers['X-Last-Fetched-At']
end
+ def notes_filter
+ current_user&.notes_filter_for(params[:target_type])
+ end
+
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 4bac763d000..3152a38fd8e 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
alias_method :awardable, :note
def finder_params
- params.merge(last_fetched_at: last_fetched_at)
+ params.merge(last_fetched_at: last_fetched_at, notes_filter: notes_filter)
end
def authorize_admin_note!
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index c67c2065440..817aac8b5d5 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -24,6 +24,8 @@ class NotesFinder
def execute
notes = init_collection
notes = since_fetch_at(notes)
+ notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter?
+
notes.fresh
end
@@ -134,4 +136,8 @@ class NotesFinder
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
notes.updated_after(last_fetched_at - FETCH_OVERLAP)
end
+
+ def notes_filter?
+ @params[:notes_filter].present?
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 97406fefd43..6069640b9c8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -386,8 +386,8 @@ module IssuablesHelper
{
todo_text: "Add todo",
mark_text: "Mark todo as done",
- todo_icon: (is_collapsed ? icon('plus-square') : nil),
- mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
+ todo_icon: (is_collapsed ? sprite_icon('todo-add') : nil),
+ mark_icon: (is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : nil),
issuable_id: issuable.id,
issuable_type: issuable.class.name.underscore,
url: project_todos_path(@project),
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 2b28b702b05..34a889057ab 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -19,7 +19,9 @@ module Ci
sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
- dast: 'gl-dast-report.json'
+ dast: 'gl-dast-report.json',
+ license_management: 'gl-license-management-report.json',
+ performance: 'performance.json'
}.freeze
TYPE_AND_FORMAT_PAIRS = {
@@ -35,7 +37,9 @@ module Ci
sast: :raw,
dependency_scanning: :raw,
container_scanning: :raw,
- dast: :raw
+ dast: :raw,
+ license_management: :raw,
+ performance: :raw
}.freeze
belongs_to :project
@@ -80,7 +84,9 @@ module Ci
dependency_scanning: 6, ## EE-specific
container_scanning: 7, ## EE-specific
dast: 8, ## EE-specific
- codequality: 9 ## EE-specific
+ codequality: 9, ## EE-specific
+ license_management: 10, ## EE-specific
+ performance: 11 ## EE-specific
}
enum file_format: {
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 17024e8a0af..aeee7f0a5d2 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -268,6 +268,12 @@ module Ci
stage unless stage.statuses_count.zero?
end
+ def ref_exists?
+ project.repository.ref_exists?(git_ref)
+ rescue Gitlab::Git::Repository::NoRepository
+ false
+ end
+
##
# TODO We do not completely switch to persisted stages because of
# race conditions with setting statuses gitlab-ce#23257.
@@ -674,11 +680,11 @@ module Ci
def push_details
strong_memoize(:push_details) do
- Gitlab::Git::Push.new(project, before_sha, sha, push_ref)
+ Gitlab::Git::Push.new(project, before_sha, sha, git_ref)
end
end
- def push_ref
+ def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 43bf852c7ec..b311f5e0617 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ActiveRecord::Base
- VERSION = '0.1.34'.freeze
+ VERSION = '0.1.35'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/note.rb b/app/models/note.rb
index 95e1d3afa00..e1bd943e8e4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -110,6 +110,15 @@ class Note < ActiveRecord::Base
:system_note_metadata, :note_diff_file)
end
+ scope :with_notes_filter, -> (notes_filter) do
+ case notes_filter
+ when UserPreference::NOTES_FILTERS[:only_comments]
+ user
+ else
+ all
+ end
+ end
+
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
scope :new_diff_notes, -> { where(type: 'DiffNote') }
scope :non_diff_notes, -> { where(type: ['Note', 'DiscussionNote', nil]) }
diff --git a/app/models/project.rb b/app/models/project.rb
index be99408fcea..382fb4f463a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -548,6 +548,8 @@ class Project < ActiveRecord::Base
self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
+ alias_method :lfs_enabled, :lfs_enabled?
+
def auto_devops_enabled?
if auto_devops&.enabled.nil?
has_auto_devops_implicitly_enabled?
diff --git a/app/models/user.rb b/app/models/user.rb
index 34efb22b359..ca7fc3b058f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -152,6 +152,7 @@ class User < ActiveRecord::Base
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
has_one :status, class_name: 'UserStatus'
+ has_one :user_preference
#
# Validations
@@ -224,6 +225,8 @@ class User < ActiveRecord::Base
enum project_view: [:readme, :activity, :files]
delegate :path, to: :namespace, allow_nil: true, prefix: true
+ delegate :notes_filter_for, to: :user_preference
+ delegate :set_notes_filter, to: :user_preference
state_machine :state, initial: :active do
event :block do
@@ -1367,6 +1370,11 @@ class User < ActiveRecord::Base
!consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user?
end
+ # Avoid migrations only building user preference object when needed.
+ def user_preference
+ super.presence || build_user_preference
+ end
+
def todos_limited_to(ids)
todos.where(id: ids)
end
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
new file mode 100644
index 00000000000..6cd91abc261
--- /dev/null
+++ b/app/models/user_preference.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class UserPreference < ActiveRecord::Base
+ # We could use enums, but Rails 4 doesn't support multiple
+ # enum options with same name for multiple fields, also it creates
+ # extra methods that aren't really needed here.
+ NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze
+
+ belongs_to :user
+
+ validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
+
+ class << self
+ def notes_filters
+ {
+ s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes],
+ s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments]
+ }
+ end
+ end
+
+ def set_notes_filter(filter_id, issuable)
+ # No need to update the column if the value is already set.
+ if filter_id && NOTES_FILTERS.values.include?(filter_id)
+ field = notes_filter_field_for(issuable)
+ self[field] = filter_id
+
+ save if attribute_changed?(field)
+ end
+
+ notes_filter_for(issuable)
+ end
+
+ # Returns the current discussion filter for a given issuable
+ # or issuable type.
+ def notes_filter_for(resource)
+ self[notes_filter_field_for(resource)]
+ end
+
+ private
+
+ def notes_filter_field_for(resource)
+ field_key =
+ if resource.is_a?(Issuable)
+ resource.model_name.param_key
+ else
+ resource
+ end
+
+ "#{field_key}_notes_filter"
+ end
+end
diff --git a/app/serializers/current_user_entity.rb b/app/serializers/current_user_entity.rb
new file mode 100644
index 00000000000..71d14e727dd
--- /dev/null
+++ b/app/serializers/current_user_entity.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Always use this entity when rendering data for current user
+# for attributes that does not need to be visible to other users
+# like user preferences.
+class CurrentUserEntity < UserEntity
+ expose :user_preference, using: UserPreferenceEntity
+end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
index fd2d2897113..53257b0602c 100644
--- a/app/serializers/merge_request_user_entity.rb
+++ b/app/serializers/merge_request_user_entity.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class MergeRequestUserEntity < UserEntity
+class MergeRequestUserEntity < CurrentUserEntity
include RequestAwareEntity
include BlobHelper
include TreeHelper
diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb
new file mode 100644
index 00000000000..fbdaab459b3
--- /dev/null
+++ b/app/serializers/user_preference_entity.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class UserPreferenceEntity < Grape::Entity
+ expose :issue_notes_filter
+ expose :merge_request_notes_filter
+
+ expose :notes_filters do |user_preference|
+ UserPreference.notes_filters
+ end
+end
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 28998acdc13..4917f4b8903 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -10,4 +10,4 @@
noteable_data: serialize_issuable(@issue),
noteable_type: 'Issue',
target_type: 'issue',
- current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
+ current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json } }
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index a678cb6f058..5374f4a1de0 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -8,12 +8,13 @@
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
- .create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
+ .create-mr-dropdown-wrap.d-inline-block{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
+
.btn-group.available.hidden
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index c39fd0063be..b50b3ca207b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -77,11 +77,12 @@
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
- .content-block.emoji-block
+ .content-block.emoji-block.emoji-block-sticky
.row
- .col-sm-8.js-noteable-awards
+ .col-md-12.col-lg-6.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
- .col-sm-4.new-branch-col
+ .col-md-12.col-lg-6.new-branch-col
+ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' unless @issue.confidential?
%section.issuable-discussion
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index ef2fa8668c0..efc2d88172e 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -51,8 +51,10 @@
= tab_link_for @merge_request, :diffs do
Changes
%span.badge.badge-pill= @merge_request.diff_size
-
- #js-vue-discussion-counter
+ .d-inline-flex.flex-wrap
+ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
+ notes_filters: UserPreference.notes_filters.to_json } }
+ #js-vue-discussion-counter
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index dbb563f51ea..2575efc0981 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -13,7 +13,11 @@
= pluralize @pipeline.total_size, "job"
- if @pipeline.ref
from
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - if @pipeline.ref_exists?
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - else
+ %span.ref-name
+ = @pipeline.ref
- if @pipeline.duration
in
= time_interval_in_words(@pipeline.duration)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index c4d177361e7..cb45928d9a5 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -36,7 +36,7 @@
%button.btn.btn-link{ type: 'button' }
= sprite_icon('search')
%span
- Press Enter or click to search
+ = _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
@@ -61,7 +61,7 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- No Assignee
+ = _('No Assignee')
%li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
@@ -74,13 +74,16 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- No Milestone
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link{ type: 'button' }
- Upcoming
+ = _('Upcoming')
%li.filter-dropdown-item{ 'data-value' => 'started' }
%button.btn.btn-link{ type: 'button' }
- Started
+ = _('Started')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
@@ -90,7 +93,7 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- No Label
+ = _('No Label')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index 583b33a8a1b..660ee6d5777 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -1,6 +1,6 @@
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
-- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
-- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
+- mark_content = is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : _('Mark todo as done')
+- todo_content = is_collapsed ? sprite_icon('todo-add') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 362569bfbaf..f62eed694d2 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -24,7 +24,7 @@
%td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= @runner.ref_protected? ? 'Yes' : 'No'
%tr
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
diff --git a/changelogs/unreleased/26723-discussion-filters.yml b/changelogs/unreleased/26723-discussion-filters.yml
new file mode 100644
index 00000000000..3abe95bf30d
--- /dev/null
+++ b/changelogs/unreleased/26723-discussion-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Filter notes by comments or activity for issues and merge requests
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/32959-update-todo-icon.yml b/changelogs/unreleased/32959-update-todo-icon.yml
new file mode 100644
index 00000000000..f08fd6aa89f
--- /dev/null
+++ b/changelogs/unreleased/32959-update-todo-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Update Todo icons in collapsed sidebar for Issues and MRs
+merge_request: 22534
+author:
+type: changed
diff --git a/changelogs/unreleased/42611-removed-branch-link.yml b/changelogs/unreleased/42611-removed-branch-link.yml
new file mode 100644
index 00000000000..03a206871b4
--- /dev/null
+++ b/changelogs/unreleased/42611-removed-branch-link.yml
@@ -0,0 +1,5 @@
+---
+title: Only render link to branch when branch still exists in pipeline page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/52059-filter-milestone-by-none-any.yml b/changelogs/unreleased/52059-filter-milestone-by-none-any.yml
new file mode 100644
index 00000000000..5511440c0b9
--- /dev/null
+++ b/changelogs/unreleased/52059-filter-milestone-by-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Added `Any` option to milestones filter
+merge_request: 22351
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/52840-fix-runners-details-page.yml b/changelogs/unreleased/52840-fix-runners-details-page.yml
new file mode 100644
index 00000000000..b061390fcf0
--- /dev/null
+++ b/changelogs/unreleased/52840-fix-runners-details-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix rendering of 'Protected' value on Runner details page
+merge_request: 22459
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-new-kubernetes-spec-helpers.yml b/changelogs/unreleased/add-new-kubernetes-spec-helpers.yml
new file mode 100644
index 00000000000..87023ede020
--- /dev/null
+++ b/changelogs/unreleased/add-new-kubernetes-spec-helpers.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce new kubernetes helpers
+merge_request: 22525
+author:
+type: other
diff --git a/changelogs/unreleased/add-role-binding-to-kubeclient.yml b/changelogs/unreleased/add-role-binding-to-kubeclient.yml
new file mode 100644
index 00000000000..bc343116eb4
--- /dev/null
+++ b/changelogs/unreleased/add-role-binding-to-kubeclient.yml
@@ -0,0 +1,5 @@
+---
+title: Allow kubeclient to call RoleBinding methods
+merge_request: 22524
+author:
+type: other
diff --git a/changelogs/unreleased/lfs-project-attribute-alias.yml b/changelogs/unreleased/lfs-project-attribute-alias.yml
new file mode 100644
index 00000000000..883869f651a
--- /dev/null
+++ b/changelogs/unreleased/lfs-project-attribute-alias.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve LFS not correctly showing enabled
+merge_request: 22501
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-pages-eof-error.yml b/changelogs/unreleased/sh-pages-eof-error.yml
new file mode 100644
index 00000000000..497a74c1458
--- /dev/null
+++ b/changelogs/unreleased/sh-pages-eof-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EOF detection with CI artifacts metadata
+merge_request: 22479
+author:
+type: fixed
diff --git a/changelogs/unreleased/support-license-management-and-performance.yml b/changelogs/unreleased/support-license-management-and-performance.yml
new file mode 100644
index 00000000000..2e65dba5e76
--- /dev/null
+++ b/changelogs/unreleased/support-license-management-and-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Support licenses and performance
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/update-runner-chart-to-0-1-35.yml b/changelogs/unreleased/update-runner-chart-to-0-1-35.yml
new file mode 100644
index 00000000000..3b8029c8d96
--- /dev/null
+++ b/changelogs/unreleased/update-runner-chart-to-0-1-35.yml
@@ -0,0 +1,5 @@
+---
+title: Update used version of Runner Helm Chart to 0.1.35
+merge_request: 22541
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 749cdd0f869..a4db125f831 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -772,9 +772,6 @@ test:
default:
path: tmp/tests/repositories/
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
- broken:
- path: tmp/tests/non-existent-repositories
- gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
gitaly:
client_path: tmp/tests/gitaly
diff --git a/db/migrate/20180925200829_create_user_preferences.rb b/db/migrate/20180925200829_create_user_preferences.rb
new file mode 100644
index 00000000000..755cabdabde
--- /dev/null
+++ b/db/migrate/20180925200829_create_user_preferences.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreateUserPreferences < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class UserPreference < ActiveRecord::Base
+ self.table_name = 'user_preferences'
+
+ NOTES_FILTERS = { all_notes: 0, comments: 1 }.freeze
+ end
+
+ def change
+ create_table :user_preferences do |t|
+ t.references :user,
+ null: false,
+ index: { unique: true },
+ foreign_key: { on_delete: :cascade }
+
+ t.integer :issue_notes_filter,
+ default: UserPreference::NOTES_FILTERS[:all_notes],
+ null: false, limit: 2
+
+ t.integer :merge_request_notes_filter,
+ default: UserPreference::NOTES_FILTERS[:all_notes],
+ null: false,
+ limit: 2
+
+ t.timestamps_with_timezone null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 50989960aa9..ddfccbba678 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2134,6 +2134,16 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_index "user_interacted_projects", ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
add_index "user_interacted_projects", ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
+ create_table "user_preferences", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "issue_notes_filter", limit: 2, default: 0, null: false
+ t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "user_preferences", ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
+
create_table "user_statuses", primary_key: "user_id", force: :cascade do |t|
t.integer "cached_markdown_version"
t.string "emoji", default: "speech_balloon", null: false
@@ -2460,6 +2470,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade
add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade
+ add_foreign_key "user_preferences", "users", on_delete: :cascade
add_foreign_key "user_statuses", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users", "application_setting_terms", column: "accepted_term_id", name: "fk_789cd90b35", on_delete: :cascade
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index e2eea57d694..a7470d27b4b 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -339,6 +339,23 @@ args: {
}
```
+### `uid_attribute`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/43806) in GitLab 10.7.
+
+By default, the `uid` is set as the `name_id` in the SAML response. If you'd like to designate a unique attribute for the `uid`, you can set the `uid_attribute`. In the example below, the value of `uid` attribute in the SAML response is set as the `uid_attribute`.
+
+```yaml
+args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ uid_attribute: 'uid'
+}
+```
+
## Troubleshooting
### 500 error after login
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 632253db94c..3cf46231a9d 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -68,7 +68,8 @@ From [project issue boards](../issue_board.md), you can filter by both group mil
When filtering by milestone, in addition to choosing a specific project milestone or group milestone, you can choose a special milestone filter.
-- **No Milestone**: Show issues or merge requests with no assigned milestone.
+- **None**: Show issues or merge requests with no assigned milestone.
+- **Any**: Show issues or merge requests that have an assigned milestone.
- **Upcoming**: Show issues or merge requests that have been assigned the open milestone that has the next upcoming due date (i.e. nearest due date in the future).
- **Started**: Show issues or merge requests that have an assigned milestone with a start date that is before today.
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 375d8bc1ff5..551d4f4473e 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -59,9 +59,12 @@ module Gitlab
until gz.eof?
begin
- path = read_string(gz).force_encoding('UTF-8')
- meta = read_string(gz).force_encoding('UTF-8')
+ path = read_string(gz)&.force_encoding('UTF-8')
+ meta = read_string(gz)&.force_encoding('UTF-8')
+ # We might hit an EOF while reading either value, so we should
+ # abort if we don't get any data.
+ next unless path && meta
next unless path.valid_encoding? && meta.valid_encoding?
next unless path =~ match_pattern
next if path =~ INVALID_PATH_PATTERN
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 98f12c226b3..3ac2a6fa777 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -11,7 +11,7 @@ module Gitlab
include Validatable
include Attributable
- ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast].freeze
+ ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
attributes ALLOWED_KEYS
@@ -26,6 +26,8 @@ module Gitlab
validates :dependency_scanning, array_of_strings_or_string: true
validates :container_scanning, array_of_strings_or_string: true
validates :dast, array_of_strings_or_string: true
+ validates :performance, array_of_strings_or_string: true
+ validates :license_management, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 30541ee3553..a17f27a3147 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -937,7 +937,7 @@ database (#{dbname}) using a super user and running:
For MySQL you instead need to run:
- GRANT ALL PRIVILEGES ON *.* TO #{user}@'%'
+ GRANT ALL PRIVILEGES ON #{dbname}.* TO #{user}@'%'
Both queries will grant the user super user permissions, ensuring you don't run
into similar problems in the future (e.g. when new tables are created).
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index 588238de608..e88a15b8acd 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -45,6 +45,13 @@ module Gitlab
:update_cluster_role_binding,
to: :rbac_client
+ # RBAC methods delegates to the apis/rbac.authorization.k8s.io api
+ # group client
+ delegate :create_role_binding,
+ :get_role_binding,
+ :update_role_binding,
+ to: :rbac_client
+
# Deployments resource is currently on the apis/extensions api group
delegate :get_deployments,
to: :extensions_client
diff --git a/lib/gitlab/kubernetes/role_binding.rb b/lib/gitlab/kubernetes/role_binding.rb
new file mode 100644
index 00000000000..4f3ee040bf2
--- /dev/null
+++ b/lib/gitlab/kubernetes/role_binding.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class RoleBinding
+ attr_reader :role_name, :namespace, :service_account_name
+
+ def initialize(role_name:, namespace:, service_account_name:)
+ @role_name = role_name
+ @namespace = namespace
+ @service_account_name = service_account_name
+ end
+
+ def generate
+ ::Kubeclient::Resource.new.tap do |resource|
+ resource.metadata = metadata
+ resource.roleRef = role_ref
+ resource.subjects = subjects
+ end
+ end
+
+ private
+
+ def metadata
+ { name: "gitlab-#{namespace}", namespace: namespace }
+ end
+
+ def role_ref
+ {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'Role',
+ name: role_name
+ }
+ end
+
+ def subjects
+ [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 4a745147858..2b7e12639be 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -32,7 +32,10 @@ module Gitlab
end
if Rails.env.test?
- storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
+ storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s
+
+ FileUtils.mkdir(storage_path) unless File.exist?(storage_path)
+ storages << { name: 'test_second_storage', path: storage_path }
end
config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bb18d4eccd8..40d45d0dee9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4027,6 +4027,12 @@ msgstr ""
msgid "No"
msgstr ""
+msgid "No Assignee"
+msgstr ""
+
+msgid "No Label"
+msgstr ""
+
msgid "No assignee"
msgstr ""
@@ -4132,6 +4138,12 @@ msgstr ""
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
+msgid "Notes|Show all activity"
+msgstr ""
+
+msgid "Notes|Show comments only"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -5589,6 +5601,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
+msgid "Something went wrong while fetching comments. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
@@ -6585,6 +6600,9 @@ msgstr ""
msgid "Up to date"
msgstr ""
+msgid "Upcoming"
+msgstr ""
+
msgid "Update"
msgstr ""
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 78293464265..d372bcbdab1 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -47,15 +47,23 @@ function create_secret() {
--dry-run -o json | kubectl apply -f -
}
+function deployExists() {
+ local namespace="${1}"
+ local deploy="${2}"
+ helm status --tiller-namespace "${namespace}" "${deploy}" >/dev/null 2>&1
+ return $?
+}
+
function previousDeployFailed() {
set +e
- echo "Checking for previous deployment of $CI_ENVIRONMENT_SLUG"
- deployment_status=$(helm status $CI_ENVIRONMENT_SLUG >/dev/null 2>&1)
+ deploy="${1}"
+ echo "Checking for previous deployment of ${deploy}"
+ deployment_status=$(helm status ${deploy} >/dev/null 2>&1)
status=$?
# if `status` is `0`, deployment exists, has a status
if [ $status -eq 0 ]; then
echo "Previous deployment found, checking status"
- deployment_status=$(helm status $CI_ENVIRONMENT_SLUG | grep ^STATUS | cut -d' ' -f2)
+ deployment_status=$(helm status ${deploy} | grep ^STATUS | cut -d' ' -f2)
echo "Previous deployment state: $deployment_status"
if [[ "$deployment_status" == "FAILED" || "$deployment_status" == "PENDING_UPGRADE" || "$deployment_status" == "PENDING_INSTALL" ]]; then
status=0;
@@ -113,7 +121,7 @@ function deploy() {
fi
# Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
- if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed ; then
+ if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then
echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG"
delete
cleanup
@@ -149,6 +157,7 @@ HELM_CMD=$(cat << EOF
--set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \
--set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \
--set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_NAME" \
+ --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \
--namespace="$KUBE_NAMESPACE" \
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
"$name" \
@@ -182,3 +191,23 @@ function cleanup() {
| xargs kubectl -n "$KUBE_NAMESPACE" delete \
|| true
}
+
+function install_external_dns() {
+ local release_name="dns-gitlab-review-app"
+ local domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}')
+
+ if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then
+ echo "Installing external-dns helm chart"
+ helm repo update
+ helm install stable/external-dns \
+ -n "${release_name}" \
+ --namespace "${KUBE_NAMESPACE}" \
+ --set provider="aws" \
+ --set aws.secretKey="${REVIEW_APPS_AWS_SECRET_KEY}" \
+ --set aws.accessKey="${REVIEW_APPS_AWS_ACCESS_KEY}" \
+ --set aws.zoneType="public" \
+ --set domainFilters[0]="${domain}" \
+ --set txtOwnerId="${KUBE_NAMESPACE}" \
+ --set rbac.create="true"
+ fi
+}
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 9df77560320..80138183c07 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1028,6 +1028,13 @@ describe Projects::IssuesController do
.not_to exceed_query_limit(control)
end
+ context 'when user is setting notes filters' do
+ let(:issuable) { issue }
+ let!(:discussion_note) { create(:discussion_note_on_issue, :system, noteable: issuable, project: project) }
+
+ it_behaves_like 'issuable notes filter'
+ end
+
context 'with cross-reference system note', :request_store do
let(:new_issue) { create(:issue) }
let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 78581dc37a4..dcfd6c05200 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -87,6 +87,14 @@ describe Projects::MergeRequestsController do
end
end
+ context 'when user is setting notes filters' do
+ let(:issuable) { merge_request }
+ let!(:discussion_note) { create(:discussion_note_on_merge_request, :system, noteable: issuable, project: project) }
+ let!(:discussion_comment) { create(:discussion_note_on_merge_request, noteable: issuable, project: project) }
+
+ it_behaves_like 'issuable notes filter'
+ end
+
describe 'as json' do
context 'with basic serializer param' do
it 'renders basic MR entity as json' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index e48c9dea976..9ac7b8ee8a8 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -47,6 +47,37 @@ describe Projects::NotesController do
get :index, request_params
end
+ context 'when user notes_filter is present' do
+ let(:notes_json) { parsed_response[:notes] }
+ let!(:comment) { create(:note, noteable: issue, project: project) }
+ let!(:system_note) { create(:note, noteable: issue, project: project, system: true) }
+
+ it 'filters system notes by comments' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue)
+
+ get :index, request_params
+
+ expect(notes_json.count).to eq(1)
+ expect(notes_json.first[:id].to_i).to eq(comment.id)
+ end
+
+ it 'returns all notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:all_notes], issue)
+
+ get :index, request_params
+
+ expect(notes_json.map { |note| note[:id].to_i }).to contain_exactly(comment.id, system_note.id)
+ end
+
+ it 'does not merge label event notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue)
+
+ expect(ResourceEvents::MergeIntoNotesService).not_to receive(:new)
+
+ get :index, request_params
+ end
+ end
+
context 'for a discussion note' do
let(:project) { create(:project, :repository) }
let!(:note) { create(:discussion_note_on_merge_request, project: project) }
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index f564e7bee47..24e70913b87 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -47,5 +47,15 @@ FactoryBot.define do
trait :ref_protected do
access_level :ref_protected
end
+
+ trait :tagged_only do
+ run_untagged false
+
+ tag_list %w(tag1 tag2)
+ end
+
+ trait :locked do
+ locked true
+ end
end
end
diff --git a/spec/factories/user_preferences.rb b/spec/factories/user_preferences.rb
new file mode 100644
index 00000000000..19059a93625
--- /dev/null
+++ b/spec/factories/user_preferences.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user_preference do
+ user
+
+ trait :only_comments do
+ issue_notes_filter { UserPreference::NOTES_FILTERS[:only_comments] }
+ merge_request_notes_filter { UserPreference::NOTE_FILTERS[:only_comments] }
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index f76d30056da..ef5801e61e8 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -189,13 +189,21 @@ describe 'Dropdown milestone', :js do
end
it 'selects `no milestone`' do
- click_static_milestone('No Milestone')
+ click_static_milestone('None')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect_tokens([milestone_token('none', false)])
expect_filtered_search_input_empty
end
+ it 'selects `any milestone`' do
+ click_static_milestone('Any')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect_tokens([milestone_token('any', false)])
+ expect_filtered_search_input_empty
+ end
+
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 491c64fc329..cd6c37bf54d 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -68,6 +68,10 @@ describe 'Pipeline', :js do
expect(page).to have_css('#js-tab-pipeline.active')
end
+ it 'shows link to the pipeline ref' do
+ expect(page).to have_link(pipeline.ref)
+ end
+
it_behaves_like 'showing user status' do
let(:user_with_status) { pipeline.user }
@@ -236,6 +240,20 @@ describe 'Pipeline', :js do
it { expect(page).not_to have_content('Cancel running') }
end
end
+
+ context 'when pipeline ref does not exist in repository anymore' do
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project,
+ ref: 'non-existent',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ it 'does not render link to the pipeline ref' do
+ expect(page).not_to have_link(pipeline.ref)
+ expect(page).to have_content(pipeline.ref)
+ end
+ end
end
context 'when user does not have access to read jobs' do
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index b776e9d856a..de9974c45e1 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -9,6 +9,27 @@ describe NotesFinder do
end
describe '#execute' do
+ context 'when notes filter is present' do
+ let!(:comment) { create(:note_on_issue, project: project) }
+ let!(:system_note) { create(:note_on_issue, project: project, system: true) }
+
+ it 'filters system notes' do
+ finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments])
+
+ notes = finder.execute
+
+ expect(notes).to match_array(comment)
+ end
+
+ it 'gets all notes' do
+ finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity])
+
+ notes = finder.execute
+
+ expect(notes).to match_array([comment, system_note])
+ end
+ end
+
it 'finds notes on merge requests' do
create(:note_on_merge_request, project: project)
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index bdee85f90b1..dc5737558c0 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -45,8 +45,10 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
expect(document.querySelector('.js-issuable-todo.sidebar-collapsed-icon')).not.toBeNull();
expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-plus-square'),
- ).not.toBeNull();
+ document
+ .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg use')
+ .getAttribute('xlink:href'),
+ ).toContain('todo-add');
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
@@ -68,8 +70,10 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
).not.toBeNull();
expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
- ).not.toBeNull();
+ document
+ .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone use')
+ .getAttribute('xlink:href'),
+ ).toContain('todo-done');
done();
});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
new file mode 100644
index 00000000000..70dd5bb3be5
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -0,0 +1,60 @@
+import Vue from 'vue';
+import createStore from '~/notes/stores';
+import DiscussionFilter from '~/notes/components/discussion_filter.vue';
+import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { discussionFiltersMock, discussionMock } from '../mock_data';
+
+describe('DiscussionFilter component', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+
+ const discussions = [{
+ ...discussionMock,
+ id: discussionMock.id,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ }];
+ const Component = Vue.extend(DiscussionFilter);
+ const defaultValue = discussionFiltersMock[0].value;
+
+ store.state.discussions = discussions;
+ vm = mountComponentWithStore(Component, {
+ el: null,
+ store,
+ props: {
+ filters: discussionFiltersMock,
+ defaultValue,
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the all filters', () => {
+ expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length);
+ });
+
+ it('renders the default selected item', () => {
+ expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title);
+ });
+
+ it('updates to the selected item', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ filterItem.click();
+
+ expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim());
+ });
+
+ it('only updates when selected filter changes', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+
+ spyOn(vm, 'filterDiscussion');
+ filterItem.click();
+
+ expect(vm.filterDiscussion).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 3e289a6b8e6..06b30375306 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -97,8 +97,7 @@ describe('note_app', () => {
});
it('should render list of notes', done => {
- const note =
- mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
+ const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0];
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 9a0e7f34a9c..ad0e793b915 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -1244,3 +1244,18 @@ export const discussion3 = {
export const unresolvableDiscussion = {
resolvable: false,
};
+
+export const discussionFiltersMock = [
+ {
+ title: 'Show all activity',
+ value: 0,
+ },
+ {
+ title: 'Show comments only',
+ value: 1,
+ },
+ {
+ title: 'Show system notes only',
+ value: 2,
+ },
+];
diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb
index 09bf21b5946..292ab870dad 100644
--- a/spec/lib/gitaly/server_spec.rb
+++ b/spec/lib/gitaly/server_spec.rb
@@ -26,9 +26,7 @@ describe Gitaly::Server do
end
end
- context 'when the storage is not readable' do
- let(:server) { described_class.new('broken') }
-
+ context 'when the storage is not readable', :broken_storage do
it 'returns false' do
expect(server).not_to be_readable
end
@@ -42,9 +40,7 @@ describe Gitaly::Server do
end
end
- context 'when the storage is not writeable' do
- let(:server) { described_class.new('broken') }
-
+ context 'when the storage is not writeable', :broken_storage do
it 'returns false' do
expect(server).not_to be_writeable
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index e327399d82d..a9a4af1f455 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -112,4 +112,34 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do
end
end
end
+
+ context 'generated metadata' do
+ let(:tmpfile) { Tempfile.new('test-metadata') }
+ let(:generator) { CiArtifactMetadataGenerator.new(tmpfile) }
+ let(:entry_count) { 5 }
+
+ before do
+ tmpfile.binmode
+
+ (1..entry_count).each do |index|
+ generator.add_entry("public/test-#{index}.txt")
+ end
+
+ generator.write
+ end
+
+ after do
+ File.unlink(tmpfile.path)
+ end
+
+ describe '#find_entries!' do
+ it 'reads expected number of entries' do
+ stream = File.open(tmpfile.path)
+
+ metadata = described_class.new(stream, 'public', { recursive: true })
+
+ expect(metadata.find_entries!.count).to eq entry_count
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 7cf541447ce..8095a231cf3 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -38,6 +38,8 @@ describe Gitlab::Ci::Config::Entry::Reports do
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
:dast | 'gl-dast-report.json'
+ :license_management | 'gl-license-management-report.json'
+ :performance | 'performance.json'
end
with_them do
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 7ebfc61f5e7..b0570680d5a 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -335,7 +335,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
restored_project_json
- expect(project.lfs_enabled).to be_nil
+ expect(project.lfs_enabled).to be_falsey
end
end
diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
new file mode 100644
index 00000000000..da3f5d27b25
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::RoleBinding, '#generate' do
+ let(:role_name) { 'edit' }
+ let(:namespace) { 'my-namespace' }
+ let(:service_account_name) { 'my-service-account' }
+
+ let(:subjects) do
+ [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
+ end
+
+ let(:role_ref) do
+ {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'Role',
+ name: role_name
+ }
+ end
+
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: "gitlab-#{namespace}", namespace: namespace },
+ roleRef: role_ref,
+ subjects: subjects
+ )
+ end
+
+ subject do
+ described_class.new(
+ role_name: role_name,
+ namespace: namespace,
+ service_account_name: service_account_name
+ ).generate
+ end
+
+ it 'should build a Kubeclient Resource' do
+ is_expected.to eq(resource)
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3b01b39ecab..153244b2159 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -779,6 +779,41 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'ref_exists?' do
+ context 'when repository exists' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project) { create(:project, :repository) }
+
+ where(:tag, :ref, :result) do
+ false | 'master' | true
+ false | 'non-existent-branch' | false
+ true | 'v1.1.0' | true
+ true | 'non-existent-tag' | false
+ end
+
+ with_them do
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project, tag: tag, ref: ref)
+ end
+
+ it "correctly detects ref" do
+ expect(pipeline.ref_exists?).to be result
+ end
+ end
+ end
+
+ context 'when repository does not exist' do
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project, ref: 'master')
+ end
+
+ it 'always returns false' do
+ expect(pipeline.ref_exists?).to eq false
+ end
+ end
+ end
+
context 'with non-empty project' do
let(:project) { create(:project, :repository) }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 23643d1c4d2..d5fb1a9d010 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -17,7 +17,7 @@ describe Clusters::Applications::Runner do
let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') }
it 'updates the application version' do
- expect(application.reload.version).to eq('0.1.34')
+ expect(application.reload.version).to eq('0.1.35')
end
end
end
@@ -45,7 +45,7 @@ describe Clusters::Applications::Runner do
it 'should be initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.1.34')
+ expect(subject.version).to eq('0.1.35')
expect(subject).not_to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -63,7 +63,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'should be initialized with the locked version' do
- expect(subject.version).to eq('0.1.34')
+ expect(subject.version).to eq('0.1.35')
end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1783dd3206b..f9be61e4768 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -865,5 +865,29 @@ describe Note do
note.save!
end
end
+
+ describe '#with_notes_filter' do
+ let!(:comment) { create(:note) }
+ let!(:system_note) { create(:note, system: true) }
+
+ context 'when notes filter is nil' do
+ subject { described_class.with_notes_filter(nil) }
+
+ it { is_expected.to include(comment, system_note) }
+ end
+
+ context 'when notes filter is set to all notes' do
+ subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:all_notes]) }
+
+ it { is_expected.to include(comment, system_note) }
+ end
+
+ context 'when notes filter is set to only comments' do
+ subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:only_comments]) }
+
+ it { is_expected.to include(comment) }
+ it { is_expected.not_to include(system_note) }
+ end
+ end
end
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
new file mode 100644
index 00000000000..64d9d9a78b4
--- /dev/null
+++ b/spec/models/user_preference_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe UserPreference do
+ describe '#set_notes_filter' do
+ let(:issuable) { build_stubbed(:issue) }
+ let(:user_preference) { create(:user_preference) }
+ let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] }
+
+ it 'returns updated discussion filter' do
+ filter_name =
+ user_preference.set_notes_filter(only_comments, issuable)
+
+ expect(filter_name).to eq(only_comments)
+ end
+
+ it 'updates discussion filter for issuable class' do
+ user_preference.set_notes_filter(only_comments, issuable)
+
+ expect(user_preference.reload.issue_notes_filter).to eq(only_comments)
+ end
+
+ context 'when notes_filter parameter is invalid' do
+ it 'returns the current notes filter' do
+ user_preference.set_notes_filter(only_comments, issuable)
+
+ expect(user_preference.set_notes_filter(9999, issuable)).to eq(only_comments)
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 99d17f563d9..b3474e74aa4 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -715,6 +715,15 @@ describe User do
end
end
+ describe 'ensure user preference' do
+ it 'has user preference upon user initialization' do
+ user = build(:user)
+
+ expect(user.user_preference).to be_present
+ expect(user.user_preference).not_to be_persisted
+ end
+ end
+
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 642de81ed52..368abded448 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -27,6 +27,7 @@ describe Ci::RetryBuildService do
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
+ job_artifacts_license_management job_artifacts_performance
job_artifacts_codequality scheduled_at].freeze
IGNORE_ACCESSORS =
diff --git a/spec/support/helpers/ci_artifact_metadata_generator.rb b/spec/support/helpers/ci_artifact_metadata_generator.rb
new file mode 100644
index 00000000000..ef638d59d2d
--- /dev/null
+++ b/spec/support/helpers/ci_artifact_metadata_generator.rb
@@ -0,0 +1,48 @@
+# frozen_sting_literal: true
+
+# This generates fake CI metadata .gz for testing
+# Based off https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/internal/zipartifacts/metadata.go
+class CiArtifactMetadataGenerator
+ attr_accessor :entries, :output
+
+ ARTIFACT_METADATA = "GitLab Build Artifacts Metadata 0.0.2\n".freeze
+
+ def initialize(stream)
+ @entries = {}
+ @output = Zlib::GzipWriter.new(stream)
+ end
+
+ def add_entry(filename)
+ @entries[filename] = { CRC: rand(0xfffffff), Comment: FFaker::Lorem.sentence(10) }
+ end
+
+ def write
+ write_version
+ write_errors
+ write_entries
+ output.close
+ end
+
+ private
+
+ def write_version
+ write_string(ARTIFACT_METADATA)
+ end
+
+ def write_errors
+ write_string('{}')
+ end
+
+ def write_entries
+ entries.each do |filename, metadata|
+ write_string(filename)
+ write_string(metadata.to_json + "\n")
+ end
+ end
+
+ def write_string(data)
+ bytes = [data.length].pack('L>')
+ output.write(bytes)
+ output.write(data)
+ end
+end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index c077ca9f15b..a03d9c4045f 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -33,10 +33,11 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
- def stub_kubeclient_get_secret(api_url, namespace: 'default', **options)
+ def stub_kubeclient_get_secret(api_url, **options)
options[:metadata_name] ||= "default-token-1"
+ options[:namespace] ||= "default"
- WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{options[:metadata_name]}")
+ WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{options[:namespace]}/secrets/#{options[:metadata_name]}")
.to_return(kube_response(kube_v1_secret_body(options)))
end
@@ -65,6 +66,21 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
+ def stub_kubeclient_create_role_binding(api_url, namespace: 'default')
+ WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings")
+ .to_return(kube_response({}))
+ end
+
+ def stub_kubeclient_create_namespace(api_url)
+ WebMock.stub_request(:post, api_url + "/api/v1/namespaces")
+ .to_return(kube_response({}))
+ end
+
+ def stub_kubeclient_get_namespace(api_url, namespace: 'default')
+ WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}")
+ .to_return(kube_response({}))
+ end
+
def kube_v1_secret_body(**options)
{
"kind" => "SecretList",
@@ -87,7 +103,8 @@ module KubernetesHelpers
{ "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" },
{ "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" },
- { "name" => "services", "namespaced" => true, "kind" => "Service" }
+ { "name" => "services", "namespaced" => true, "kind" => "Service" },
+ { "name" => "namespaces", "namespaced" => true, "kind" => "Namespace" }
]
}
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 1a9aa252511..71287f28171 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -70,7 +70,6 @@ module TestEnv
TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
REPOS_STORAGE = 'default'.freeze
- BROKEN_STORAGE = 'broken'.freeze
# Test environment
#
@@ -159,10 +158,6 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do
- # Re-create config, to specify the broken storage path
- storage_paths = { 'default' => repos_path, 'broken' => broken_path }
- Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, storage_paths, force: true)
-
start_gitaly(gitaly_dir)
end
end
@@ -257,10 +252,6 @@ module TestEnv
@repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
end
- def broken_path
- @broken_path ||= Gitlab.config.repositories.storages[BROKEN_STORAGE].legacy_disk_path
- end
-
def backup_path
Gitlab.config.backup.path
end
diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
new file mode 100644
index 00000000000..9c9d7ad781e
--- /dev/null
+++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
@@ -0,0 +1,54 @@
+shared_examples 'issuable notes filter' do
+ it 'sets discussion filter' do
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+
+ expect(user.reload.notes_filter_for(issuable)).to eq(notes_filter)
+ expect(UserPreference.count).to eq(1)
+ end
+
+ it 'expires notes e-tag cache for issuable if filter changed' do
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+
+ expect_any_instance_of(issuable.class).to receive(:expire_note_etag_cache)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+ end
+
+ it 'does not expires notes e-tag cache for issuable if filter did not change' do
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+ user.set_notes_filter(notes_filter, issuable)
+
+ expect_any_instance_of(issuable.class).not_to receive(:expire_note_etag_cache)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+ end
+
+ it 'does not set notes filter when database is in read only mode' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+
+ expect(user.reload.notes_filter_for(issuable)).to eq(0)
+ end
+
+ it 'returns no system note' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+
+ expect(JSON.parse(response.body).count).to eq(1)
+ end
+
+ context 'when filter is set to "only_comments"' do
+ it 'does not merge label event notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable)
+
+ expect(ResourceEvents::MergeIntoNotesService).not_to receive(:new)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index 6a9ad43941d..55212355daa 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -1,8 +1,4 @@
RSpec.configure do |config|
- config.before(:all, :broken_storage) do
- FileUtils.rm_rf Gitlab.config.repositories.storages.broken.legacy_disk_path
- end
-
config.before(:each, :broken_storage) do
allow(Gitlab::GitalyClient).to receive(:call) do
raise GRPC::Unavailable.new('Gitaly broken in this spec')
diff --git a/spec/views/shared/runners/show.html.haml_spec.rb b/spec/views/shared/runners/show.html.haml_spec.rb
new file mode 100644
index 00000000000..5e92928b143
--- /dev/null
+++ b/spec/views/shared/runners/show.html.haml_spec.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'shared/runners/show.html.haml' do
+ include PageLayoutHelper
+
+ let(:runner) do
+ create(:ci_runner, name: 'test runner',
+ version: '11.4.0',
+ ip_address: '127.1.2.3',
+ revision: 'abcd1234',
+ architecture: 'amd64' )
+ end
+
+ before do
+ assign(:runner, runner)
+ end
+
+ subject do
+ render
+ rendered
+ end
+
+ describe 'Page title' do
+ before do
+ expect_any_instance_of(PageLayoutHelper).to receive(:page_title).with("#{runner.description} ##{runner.id}", 'Runners')
+ end
+
+ it 'sets proper page title' do
+ render
+ end
+ end
+
+ describe 'Runner id and type' do
+ context 'when runner is of type instance' do
+ it { is_expected.to have_content("Runner ##{runner.id} Shared") }
+ end
+
+ context 'when runner is of type group' do
+ let(:runner) { create(:ci_runner, :group) }
+
+ it { is_expected.to have_content("Runner ##{runner.id} Group") }
+ end
+
+ context 'when runner is of type project' do
+ let(:runner) { create(:ci_runner, :project) }
+
+ it { is_expected.to have_content("Runner ##{runner.id} Specific") }
+ end
+ end
+
+ describe 'Active value' do
+ context 'when runner is active' do
+ it { is_expected.to have_content('Active Yes') }
+ end
+
+ context 'when runner is inactive' do
+ let(:runner) { create(:ci_runner, :inactive) }
+
+ it { is_expected.to have_content('Active No') }
+ end
+ end
+
+ describe 'Protected value' do
+ context 'when runner is not protected' do
+ it { is_expected.to have_content('Protected No') }
+ end
+
+ context 'when runner is protected' do
+ let(:runner) { create(:ci_runner, :ref_protected) }
+
+ it { is_expected.to have_content('Protected Yes') }
+ end
+ end
+
+ describe 'Can run untagged jobs value' do
+ context 'when runner run untagged job is set' do
+ it { is_expected.to have_content('Can run untagged jobs Yes') }
+ end
+
+ context 'when runner run untagged job is unset' do
+ let(:runner) { create(:ci_runner, :tagged_only) }
+
+ it { is_expected.to have_content('Can run untagged jobs No') }
+ end
+ end
+
+ describe 'Locked to this project value' do
+ context 'when runner locked is not set' do
+ it { is_expected.to have_content('Locked to this project No') }
+
+ context 'when runner is of type group' do
+ let(:runner) { create(:ci_runner, :group) }
+
+ it { is_expected.not_to have_content('Locked to this project') }
+ end
+ end
+
+ context 'when runner locked is set' do
+ let(:runner) { create(:ci_runner, :locked) }
+
+ it { is_expected.to have_content('Locked to this project Yes') }
+
+ context 'when runner is of type group' do
+ let(:runner) { create(:ci_runner, :group, :locked) }
+
+ it { is_expected.not_to have_content('Locked to this project') }
+ end
+ end
+ end
+
+ describe 'Tags value' do
+ context 'when runner does not have tags' do
+ it { is_expected.to have_content('Tags') }
+ it { is_expected.not_to have_selector('span.badge.badge-primary')}
+ end
+
+ context 'when runner have tags' do
+ let(:runner) { create(:ci_runner, tag_list: %w(tag2 tag3 tag1)) }
+
+ it { is_expected.to have_content('Tags tag1 tag2 tag3') }
+ it { is_expected.to have_selector('span.badge.badge-primary')}
+ end
+ end
+
+ describe 'Metadata values' do
+ it { is_expected.to have_content("Name #{runner.name}") }
+ it { is_expected.to have_content("Version #{runner.version}") }
+ it { is_expected.to have_content("IP Address #{runner.ip_address}") }
+ it { is_expected.to have_content("Revision #{runner.revision}") }
+ it { is_expected.to have_content("Platform #{runner.platform}") }
+ it { is_expected.to have_content("Architecture #{runner.architecture}") }
+ it { is_expected.to have_content("Description #{runner.description}") }
+ end
+
+ describe 'Maximum job timeout value' do
+ let(:runner) { create(:ci_runner, maximum_timeout: 5400) }
+
+ it { is_expected.to have_content('Maximum job timeout 1h 30m') }
+ end
+
+ describe 'Last contact value' do
+ context 'when runner have not contacted yet' do
+ it { is_expected.to have_content('Last contact Never') }
+ end
+
+ context 'when runner have already contacted' do
+ let(:runner) { create(:ci_runner, contacted_at: DateTime.now - 6.days) }
+ let(:expected_contacted_at) { I18n.localize(runner.contacted_at, format: "%b %d, %Y") }
+
+ it { is_expected.to have_content("Last contact #{expected_contacted_at}") }
+ end
+ end
+end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index ede271b2cdd..50b93fce2dc 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -51,7 +51,7 @@ describe RepositoryCheck::BatchWorker do
it 'does nothing when shard is unhealthy' do
shard_name = 'broken'
- create(:project, created_at: 1.week.ago, repository_storage: shard_name)
+ create(:project, :broken_storage, created_at: 1.week.ago)
expect(subject.perform(shard_name)).to eq(nil)
end