diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-11-07 17:54:03 +0100 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-11-07 17:54:03 +0100 |
commit | e957ea4ae1dac758c3510288b2ac2a2992cd4f7e (patch) | |
tree | 3bda223d153d4889eb883666a1c448005ca445e6 | |
parent | 0a459db00877d032e7765ec9f3b11efcfceaf168 (diff) | |
parent | 18ac8acb5b97c2a395b8c453a29d0fcfd176b9c9 (diff) | |
download | gitlab-ce-e957ea4ae1dac758c3510288b2ac2a2992cd4f7e.tar.gz |
Merge branch '38464-k8s-apps' of https://gitlab.com/gitlab-org/gitlab-ce into add-ingress-to-cluster-applications
68 files changed, 766 insertions, 442 deletions
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 7a23154b340..9f16e6bb2cc 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -1,11 +1,15 @@ <script> import { mapActions, mapGetters } from 'vuex'; import timeAgoMixin from '../../vue_shared/mixins/timeago'; + import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; export default { mixins: [ timeAgoMixin, ], + components: { + skeletonLoadingContainer, + }, props: { file: { type: Object, @@ -16,6 +20,9 @@ ...mapGetters([ 'isCollapsed', ]), + isSubmodule() { + return this.file.type === 'submodule'; + }, fileIcon() { return { 'fa-spinner fa-spin': this.file.loading, @@ -31,6 +38,9 @@ shortId() { return this.file.id.substr(0, 8); }, + submoduleColSpan() { + return !this.isCollapsed && this.isSubmodule ? 3 : 1; + }, }, methods: { ...mapActions([ @@ -44,7 +54,7 @@ <tr class="file" @click.prevent="clickedTreeRow(file)"> - <td> + <td :colspan="submoduleColSpan"> <i class="fa fa-fw file-icon" :class="fileIcon" @@ -58,7 +68,7 @@ > {{ file.name }} </a> - <template v-if="file.type === 'submodule' && file.id"> + <template v-if="isSubmodule && file.id"> @ <span class="commit-sha"> <a @@ -71,15 +81,20 @@ </template> </td> - <template v-if="!isCollapsed"> + <template v-if="!isCollapsed && !isSubmodule"> <td class="hidden-sm hidden-xs"> <a + v-if="file.lastCommit.message" @click.stop :href="file.lastCommit.url" class="commit-message" > {{ file.lastCommit.message }} </a> + <skeleton-loading-container + v-else + :small="true" + /> </td> <td class="commit-update hidden-xs text-right"> @@ -89,6 +104,11 @@ > {{ timeFormated(file.lastCommit.updatedAt) }} </span> + <skeleton-loading-container + v-else + class="animation-container-right" + :small="true" + /> </td> </template> </tr> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index 1e6c405f292..14c2ddfb615 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -1,17 +1,16 @@ <script> import { mapGetters } from 'vuex'; + import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; export default { + components: { + skeletonLoadingContainer, + }, computed: { ...mapGetters([ 'isCollapsed', ]), }, - methods: { - lineOfCode(n) { - return `skeleton-line-${n}`; - }, - }, }; </script> @@ -21,36 +20,24 @@ aria-label="Loading files" > <td> - <div - class="animation-container animation-container-small"> - <div - v-for="n in 6" - :key="n" - :class="lineOfCode(n)"> - </div> - </div> + <skeleton-loading-container + :small="true" + /> </td> <template v-if="!isCollapsed"> <td class="hidden-sm hidden-xs"> - <div class="animation-container"> - <div - v-for="n in 6" - :key="n" - :class="lineOfCode(n)"> - </div> - </div> + <skeleton-loading-container + :small="true" + /> </td> <td class="hidden-xs"> - <div class="animation-container animation-container-small animation-container-right"> - <div - v-for="n in 6" - :key="n" - :class="lineOfCode(n)"> - </div> - </div> + <skeleton-loading-container + class="animation-container-right" + :small="true" + /> </td> </template> </tr> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 63c0d70f5c0..3f61d048288 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -80,7 +80,7 @@ export default { /> <repo-file v-for="(file, index) in treeList" - :key="index" + :key="file.key" :file="file" /> </tbody> diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js index dc222ccac01..2fb45dcb03c 100644 --- a/app/assets/javascripts/repo/services/index.js +++ b/app/assets/javascripts/repo/services/index.js @@ -30,4 +30,11 @@ export default { commit(projectId, payload) { return Api.commitMultiple(projectId, payload); }, + getTreeLastCommit(endpoint) { + return Vue.http.get(endpoint, { + params: { + format: 'json', + }, + }); + }, }; diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js index ca2f2a5ce7a..be290c268b1 100644 --- a/app/assets/javascripts/repo/stores/actions.js +++ b/app/assets/javascripts/repo/stores/actions.js @@ -64,7 +64,7 @@ export const checkCommitStatus = ({ state }) => service.getBranchData( }) .catch(() => flash('Error checking branch data. Please try again.')); -export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) => +export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) => service.commit(state.project.id, payload) .then((data) => { const { branch } = payload; @@ -73,12 +73,28 @@ export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) = return; } + const lastCommit = { + commit_path: `${state.project.url}/commit/${data.id}`, + commit: { + message: data.message, + authored_date: data.committed_date, + }, + }; + flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); if (newMr) { redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`); } else { commit(types.SET_COMMIT_REF, data.id); + + getters.changedFiles.forEach((entry) => { + commit(types.SET_LAST_COMMIT_DATA, { + entry, + lastCommit, + }); + }); + dispatch('discardAllChanges'); dispatch('closeAllFiles'); dispatch('toggleEditMode'); diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js index afbe0b78a82..5bae4fa826a 100644 --- a/app/assets/javascripts/repo/stores/actions/file.js +++ b/app/assets/javascripts/repo/stores/actions/file.js @@ -27,6 +27,8 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false }) } else if (!state.openFiles.length) { pushState(file.parentTreeUrl); } + + dispatch('getLastCommitData'); }; export const setFileActive = ({ commit, state, getters, dispatch }, file) => { diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js index 129743c66c2..aa830e946a2 100644 --- a/app/assets/javascripts/repo/stores/actions/tree.js +++ b/app/assets/javascripts/repo/stores/actions/tree.js @@ -7,10 +7,11 @@ import { setPageTitle, findEntry, createTemp, + createOrMergeEntry, } from '../utils'; export const getTreeData = ( - { commit, state }, + { commit, state, dispatch }, { endpoint = state.endpoints.rootEndpoint, tree = state } = {}, ) => { commit(types.TOGGLE_LOADING, tree); @@ -24,14 +25,20 @@ export const getTreeData = ( return res.json(); }) .then((data) => { + const prevLastCommitPath = tree.lastCommitPath; if (!state.isInitialRoot) { commit(types.SET_ROOT, data.path === '/'); } - commit(types.SET_DIRECTORY_DATA, { data, tree }); + dispatch('updateDirectoryData', { data, tree }); commit(types.SET_PARENT_TREE_URL, data.parent_tree_url); + commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path }); commit(types.TOGGLE_LOADING, tree); + if (prevLastCommitPath !== null) { + dispatch('getLastCommitData', tree); + } + pushState(endpoint); }) .catch(() => { @@ -48,7 +55,7 @@ export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => { pushState(tree.parentTreeUrl); commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl); - commit(types.SET_DIRECTORY_DATA, { data, tree }); + dispatch('updateDirectoryData', { data, tree }); } else { commit(types.SET_PREVIOUS_URL, endpoint); dispatch('getTreeData', { endpoint, tree }); @@ -108,3 +115,48 @@ export const createTempTree = ({ state, commit, dispatch }, name) => { }); } }; + +export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { + if (tree.lastCommitPath === null || getters.isCollapsed) return; + + service.getTreeLastCommit(tree.lastCommitPath) + .then((res) => { + const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; + + commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); + + return res.json(); + }) + .then((data) => { + data.forEach((lastCommit) => { + const entry = findEntry(tree, lastCommit.type, lastCommit.file_name); + + if (entry) { + commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); + } + }); + + dispatch('getLastCommitData', tree); + }) + .catch(() => flash('Error fetching log data.')); +}; + +export const updateDirectoryData = ({ commit, state }, { data, tree }) => { + const level = tree.level !== undefined ? tree.level + 1 : 0; + const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl; + const createEntry = (entry, type) => createOrMergeEntry({ + tree, + entry, + level, + type, + parentTreeUrl, + }); + + const formattedData = [ + ...data.trees.map(t => createEntry(t, 'tree')), + ...data.submodules.map(m => createEntry(m, 'submodule')), + ...data.blobs.map(b => createEntry(b, 'blob')), + ]; + + commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData }); +}; diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js index 4722a7dd0df..bc3390f1506 100644 --- a/app/assets/javascripts/repo/stores/mutation_types.js +++ b/app/assets/javascripts/repo/stores/mutation_types.js @@ -4,11 +4,13 @@ export const SET_COMMIT_REF = 'SET_COMMIT_REF'; export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL'; export const SET_ROOT = 'SET_ROOT'; export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL'; +export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; // Tree mutation types export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; export const CREATE_TMP_TREE = 'CREATE_TMP_TREE'; +export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; // File mutation types export const SET_FILE_DATA = 'SET_FILE_DATA'; diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js index 2f9b038322b..ae2ba5bedf7 100644 --- a/app/assets/javascripts/repo/stores/mutations.js +++ b/app/assets/javascripts/repo/stores/mutations.js @@ -48,6 +48,13 @@ export default { previousUrl, }); }, + [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { + Object.assign(entry.lastCommit, { + url: lastCommit.commit_path, + message: lastCommit.commit.message, + updatedAt: lastCommit.commit.authored_date, + }); + }, ...fileMutations, ...treeMutations, ...branchMutations, diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js index 52be2673107..130221c9fda 100644 --- a/app/assets/javascripts/repo/stores/mutations/tree.js +++ b/app/assets/javascripts/repo/stores/mutations/tree.js @@ -1,5 +1,4 @@ import * as types from '../mutation_types'; -import * as utils from '../utils'; export default { [types.TOGGLE_TREE_OPEN](state, tree) { @@ -8,30 +7,8 @@ export default { }); }, [types.SET_DIRECTORY_DATA](state, { data, tree }) { - const level = tree.level !== undefined ? tree.level + 1 : 0; - const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl; - Object.assign(tree, { - tree: [ - ...data.trees.map(t => utils.decorateData({ - ...t, - type: 'tree', - parentTreeUrl, - level, - }, state.project.url)), - ...data.submodules.map(m => utils.decorateData({ - ...m, - type: 'submodule', - parentTreeUrl, - level, - }, state.project.url)), - ...data.blobs.map(b => utils.decorateData({ - ...b, - type: 'blob', - parentTreeUrl, - level, - }, state.project.url)), - ], + tree: data, }); }, [types.SET_PARENT_TREE_URL](state, url) { @@ -39,6 +16,11 @@ export default { parentTreeUrl: url, }); }, + [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { + Object.assign(tree, { + lastCommitPath: url, + }); + }, [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) { parent.tree.push(tmpEntry); }, diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js index aab74754f02..0068834831e 100644 --- a/app/assets/javascripts/repo/stores/state.js +++ b/app/assets/javascripts/repo/stores/state.js @@ -8,6 +8,7 @@ export default () => ({ endpoints: {}, isRoot: false, isInitialRoot: false, + lastCommitPath: '', loading: false, onTopOfBranch: false, openFiles: [], diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js index 797c2b1e5b9..fae1f4439a9 100644 --- a/app/assets/javascripts/repo/stores/utils.js +++ b/app/assets/javascripts/repo/stores/utils.js @@ -1,5 +1,6 @@ export const dataStructure = () => ({ id: '', + key: '', type: '', name: '', url: '', @@ -12,7 +13,12 @@ export const dataStructure = () => ({ opened: false, active: false, changed: false, - lastCommit: {}, + lastCommitPath: '', + lastCommit: { + url: '', + message: '', + updatedAt: '', + }, tree_url: '', blamePath: '', commitsPath: '', @@ -27,14 +33,13 @@ export const dataStructure = () => ({ base64: false, }); -export const decorateData = (entity, projectUrl = '') => { +export const decorateData = (entity) => { const { id, type, url, name, icon, - last_commit, tree_url, path, renderError, @@ -51,6 +56,7 @@ export const decorateData = (entity, projectUrl = '') => { return { ...dataStructure(), id, + key: `${name}-${type}-${id}`, type, name, url, @@ -66,12 +72,6 @@ export const decorateData = (entity, projectUrl = '') => { renderError, content, base64, - // eslint-disable-next-line camelcase - lastCommit: last_commit ? { - url: `${projectUrl}/commit/${last_commit.id}`, - message: last_commit.message, - updatedAt: last_commit.committed_date, - } : {}, }; }; @@ -106,3 +106,22 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 } renderError: base64, }); }; + +export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => { + const found = findEntry(tree, type, entry.name); + + if (found) { + return Object.assign({}, found, { + id: entry.id, + url: entry.url, + tempFile: false, + }); + } + + return decorateData({ + ...entry, + type, + parentTreeUrl, + level, + }); +}; diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 2bf7a3a5d61..8e931995fc6 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -3,9 +3,10 @@ * and controllable by a public API. */ -class SmartInterval { +export default class SmartInterval { /** - * @param { function } opts.callback Function to be called on each iteration (required) + * @param { function } opts.callback Function that returns a promise, called on each iteration + * unless still in progress (required) * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this @@ -42,13 +43,16 @@ class SmartInterval { const cfg = this.cfg; const state = this.state; - if (cfg.immediateExecution) { + if (cfg.immediateExecution && !this.isLoading) { cfg.immediateExecution = false; - cfg.callback(); + this.triggerCallback(); } state.intervalId = window.setInterval(() => { - cfg.callback(); + if (this.isLoading) { + return; + } + this.triggerCallback(); if (this.getCurrentInterval() === cfg.maxInterval) { return; @@ -76,7 +80,7 @@ class SmartInterval { // start a timer, using the existing interval resume() { - this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped + this.stopTimer(); // stop existing timer, in case timer was not previously stopped this.start(); } @@ -104,6 +108,18 @@ class SmartInterval { this.initPageUnloadHandling(); } + triggerCallback() { + this.isLoading = true; + this.cfg.callback() + .then(() => { + this.isLoading = false; + }) + .catch((err) => { + this.isLoading = false; + throw err; + }); + } + initVisibilityChangeHandling() { // cancel interval when tab no longer shown (prevents cached pages from polling) document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); @@ -154,4 +170,3 @@ class SmartInterval { } } -window.gl.SmartInterval = SmartInterval; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 4f497b204a3..aaff9ee6518 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -1,3 +1,4 @@ +import SmartInterval from '~/smart_interval'; import Flash from '../flash'; import { WidgetHeader, @@ -81,7 +82,7 @@ export default { return new MRWidgetService(endpoints); }, checkStatus(cb) { - this.service.checkStatus() + return this.service.checkStatus() .then(res => res.json()) .then((res) => { this.handleNotification(res); @@ -97,7 +98,7 @@ export default { }); }, initPolling() { - this.pollingInterval = new gl.SmartInterval({ + this.pollingInterval = new SmartInterval({ callback: this.checkStatus, startingInterval: 10000, maxInterval: 30000, @@ -106,7 +107,7 @@ export default { }); }, initDeploymentsPolling() { - this.deploymentsInterval = new gl.SmartInterval({ + this.deploymentsInterval = new SmartInterval({ callback: this.fetchDeployments, startingInterval: 30000, maxInterval: 120000, @@ -121,7 +122,7 @@ export default { } }, fetchDeployments() { - this.service.fetchDeployments() + return this.service.fetchDeployments() .then(res => res.json()) .then((res) => { if (res.length) { diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue new file mode 100644 index 00000000000..b06493e6c66 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue @@ -0,0 +1,37 @@ +<script> + export default { + props: { + small: { + type: Boolean, + required: false, + default: false, + }, + lines: { + type: Number, + required: false, + default: 6, + }, + }, + computed: { + lineClasses() { + return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`); + }, + }, + }; +</script> + +<template> + <div + class="animation-container" + :class="{ + 'animation-container-small': small, + }" + > + <div + v-for="(css, index) in lineClasses" + :key="index" + :class="css" + > + </div> + </div> +</template> diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 9c222549cdc..072dffaff7a 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -9,9 +9,7 @@ module IssuableActions def show respond_to do |format| - format.html do - render show_view - end + format.html format.json do render json: serializer.represent(issuable, serializer: params[:serializer]) end @@ -152,10 +150,6 @@ module IssuableActions end end - def show_view - 'show' - end - def serializer raise NotImplementedError end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 3181f517087..2b011bc87b0 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -4,58 +4,44 @@ module IssuableCollections include Gitlab::IssuableMetadata included do - helper_method :issues_finder - helper_method :merge_requests_finder + helper_method :finder end private - def set_issues_index - @collection_type = "Issue" - @issues = issues_collection - @issues = @issues.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues, @collection_type) - @total_pages = issues_page_count(@issues) + def set_issuables_index + @issuables = issuables_collection + @issuables = @issuables.page(params[:page]) + @issuable_meta_data = issuable_meta_data(@issuables, collection_type) + @total_pages = issuable_page_count - return if redirect_out_of_range(@issues, @total_pages) + return if redirect_out_of_range(@total_pages) if params[:label_name].present? - @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute + labels_params = { project_id: @project.id, title: params[:label_name] } + @labels = LabelsFinder.new(current_user, labels_params).execute end @users = [] - end - - def issues_collection - issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace) - end - - def merge_requests_collection - merge_requests_finder.execute.preload( - :source_project, - :target_project, - :author, - :assignee, - :labels, - :milestone, - head_pipeline: :project, - target_project: :namespace, - merge_request_diff: :merge_request_diff_commits - ) - end + if params[:assignee_id].present? + assignee = User.find_by_id(params[:assignee_id]) + @users.push(assignee) if assignee + end - def issues_finder - @issues_finder ||= issuable_finder_for(IssuesFinder) + if params[:author_id].present? + author = User.find_by_id(params[:author_id]) + @users.push(author) if author + end end - def merge_requests_finder - @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) + def issuables_collection + finder.execute.preload(preload_for_collection) end - def redirect_out_of_range(relation, total_pages) + def redirect_out_of_range(total_pages) return false if total_pages.zero? - out_of_range = relation.current_page > total_pages + out_of_range = @issuables.current_page > total_pages if out_of_range redirect_to(url_for(params.merge(page: total_pages, only_path: true))) @@ -64,12 +50,8 @@ module IssuableCollections out_of_range end - def issues_page_count(relation) - page_count_for_relation(relation, issues_finder.row_count) - end - - def merge_requests_page_count(relation) - page_count_for_relation(relation, merge_requests_finder.row_count) + def issuable_page_count + page_count_for_relation(@issuables, finder.row_count) end def page_count_for_relation(relation, row_count) @@ -145,4 +127,31 @@ module IssuableCollections else value end end + + def finder + return @finder if defined?(@finder) + + @finder = issuable_finder_for(@finder_type) + end + + def collection_type + @collection_type ||= case finder + when IssuesFinder + 'Issue' + when MergeRequestsFinder + 'MergeRequest' + end + end + + def preload_for_collection + @preload_for_collection ||= case collection_type + when 'Issue' + [:project, :author, :assignees, :labels, :milestone, project: :namespace] + when 'MergeRequest' + [ + :source_project, :target_project, :author, :assignee, :labels, :milestone, + head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits + ] + end + end end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 404559c8707..ad594903331 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -3,14 +3,14 @@ module IssuesAction include IssuableCollections def issues - @label = issues_finder.labels.first + @finder_type = IssuesFinder + @label = finder.labels.first - @issues = issues_collection + @issues = issuables_collection .non_archived .page(params[:page]) - @collection_type = "Issue" - @issuable_meta_data = issuable_meta_data(@issues, @collection_type) + @issuable_meta_data = issuable_meta_data(@issues, collection_type) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index d3c8e4888bc..8b569a01afd 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -3,13 +3,12 @@ module MergeRequestsAction include IssuableCollections def merge_requests - @label = merge_requests_finder.labels.first + @finder_type = MergeRequestsFinder + @label = finder.labels.first - @merge_requests = merge_requests_collection - .page(params[:page]) + @merge_requests = issuables_collection.page(params[:page]) - @collection_type = "MergeRequest" - @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) + @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type) end private diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d4e763aa5b8..dbc9106ba6d 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,7 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :check_issues_available! before_action :issue, except: [:index, :new, :create, :bulk_update] - before_action :set_issues_index, only: [:index] + before_action :set_issuables_index, only: [:index] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -24,15 +24,7 @@ class Projects::IssuesController < Projects::ApplicationController respond_to :html def index - if params[:assignee_id].present? - assignee = User.find_by_id(params[:assignee_id]) - @users.push(assignee) if assignee - end - - if params[:author_id].present? - author = User.find_by_id(params[:author_id]) - @users.push(author) if author - end + @issues = @issuables respond_to do |format| format.html @@ -252,4 +244,9 @@ class Projects::IssuesController < Projects::ApplicationController update_params = issue_params.merge(spammable_params) Issues::UpdateService.new(project, current_user, update_params) end + + def set_issuables_index + @finder_type = IssuesFinder + super + end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c86acae8fe4..402420b851e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,33 +10,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :set_issuables_index, only: [:index] + before_action :authenticate_user!, only: [:assign_related_issues] def index - @collection_type = "MergeRequest" - @merge_requests = merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]) - @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request) - @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) - @total_pages = merge_requests_page_count(@merge_requests) - - return if redirect_out_of_range(@merge_requests, @total_pages) - - if params[:label_name].present? - labels_params = { project_id: @project.id, title: params[:label_name] } - @labels = LabelsFinder.new(current_user, labels_params).execute - end - - @users = [] - if params[:assignee_id].present? - assignee = User.find_by_id(params[:assignee_id]) - @users.push(assignee) if assignee - end - - if params[:author_id].present? - author = User.find_by_id(params[:author_id]) - @users.push(author) if author - end + @merge_requests = @issuables respond_to do |format| format.html @@ -338,4 +317,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @target_project = @merge_request.target_project @target_branches = @merge_request.target_project.repository.branch_names end + + def set_issuables_index + @finder_type = MergeRequestsFinder + super + end end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 2fd015df688..2376f469213 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -56,9 +56,12 @@ class Projects::RefsController < Projects::ApplicationController contents[@offset, @limit].to_a.map do |content| file = @path ? File.join(@path, content.name) : content.name last_commit = @repo.last_commit_for_path(@commit.id, file) + commit_path = project_commit_path(@project, last_commit) if last_commit { file_name: content.name, - commit: last_commit + commit: last_commit, + type: content.type, + commit_path: commit_path } end end @@ -70,6 +73,11 @@ class Projects::RefsController < Projects::ApplicationController respond_to do |format| format.html { render_404 } + format.json do + response.headers["More-Logs-Url"] = @more_log_url + + render json: @logs + end format.js end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 1688121e27e..2a473ec0cec 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -275,7 +275,8 @@ class ProjectsController < Projects::ApplicationController @project_wiki = @project.wiki @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) - @issues = issues_collection.page(params[:page]) + @finder_type = IssuesFinder + @issues = issuables_collection.page(params[:page]) @collection_type = 'Issue' @issuable_meta_data = issuable_meta_data(@issues, @collection_type) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 85407e38532..a9840d19178 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -249,8 +249,6 @@ module IssuablesHelper end def issuables_count_for_state(issuable_type, state) - finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend - Gitlab::IssuablesCountForState.new(finder)[state] end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index c4391729dd7..ee2e43ee9dd 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -7,7 +7,7 @@ module Clusters default_value_for :zone, 'us-central1-a' default_value_for :num_nodes, 3 - default_value_for :machine_type, 'n1-standard-4' + default_value_for :machine_type, 'n1-standard-2' attr_encrypted :access_token, mode: :per_attribute_iv, diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index f3888528940..6b07dbdf3ea 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base delegate :sha, :short_sha, to: :pipeline validates :pipeline, presence: true, unless: :importing? - validates :name, presence: true, unless: :importing? alias_attribute :author, :user @@ -46,6 +45,17 @@ class CommitStatus < ActiveRecord::Base runner_system_failure: 4 } + ## + # We still create some CommitStatuses outside of CreatePipelineService. + # + # These are pages deployments and external statuses. + # + before_create unless: :importing? do + Ci::EnsureStageService.new(project, user).execute(self) do |stage| + self.run_after_commit { StageUpdateWorker.perform_async(stage.id) } + end + end + state_machine :status do event :process do transition [:skipped, :manual] => :created diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index a928b9d6367..c008fb91a16 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -17,6 +17,8 @@ module Issuable include Importable include Editable include AfterCommitQueue + include Sortable + include CreatedAtFilterable # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests diff --git a/app/models/issue.rb b/app/models/issue.rb index fc590f9257e..3b3c7fb7f8b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -5,11 +5,9 @@ class Issue < ActiveRecord::Base include Issuable include Noteable include Referable - include Sortable include Spammable include FasterCacheKeys include RelativePositioning - include CreatedAtFilterable include TimeTrackable DueDateStruct = Struct.new(:title, :name).freeze diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f80601f3484..82d0ae90d77 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -3,9 +3,7 @@ class MergeRequest < ActiveRecord::Base include Issuable include Noteable include Referable - include Sortable include IgnorableColumn - include CreatedAtFilterable include TimeTrackable ignore_column :locked_at, diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb index 56f173e5a27..ad039a2623d 100644 --- a/app/serializers/blob_entity.rb +++ b/app/serializers/blob_entity.rb @@ -3,10 +3,6 @@ class BlobEntity < Grape::Entity expose :id, :path, :name, :mode - expose :last_commit do |blob| - request.project.repository.last_commit_for_path(blob.commit_id, blob.path) - end - expose :icon do |blob| IconsHelper.file_type_icon_class('file', blob.mode, blob.name) end diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb index 555e5cf83bd..9f1b485347f 100644 --- a/app/serializers/tree_entity.rb +++ b/app/serializers/tree_entity.rb @@ -3,10 +3,6 @@ class TreeEntity < Grape::Entity expose :id, :path, :name, :mode - expose :last_commit do |tree| - request.project.repository.last_commit_for_path(tree.commit_id, tree.path) - end - expose :icon do |tree| IconsHelper.file_type_icon_class('folder', tree.mode, tree.name) end diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb index 69702ae1493..496f070ddbd 100644 --- a/app/serializers/tree_root_entity.rb +++ b/app/serializers/tree_root_entity.rb @@ -18,4 +18,8 @@ class TreeRootEntity < Grape::Entity project_tree_path(request.project, File.join(request.ref, parent_tree_path)) end + + expose :last_commit_path do |tree| + logs_file_project_ref_path(request.project, request.ref, tree.path) + end end diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb new file mode 100644 index 00000000000..dc2f49e8db1 --- /dev/null +++ b/app/services/ci/ensure_stage_service.rb @@ -0,0 +1,39 @@ +module Ci + ## + # We call this service everytime we persist a CI/CD job. + # + # In most cases a job should already have a stage assigned, but in cases it + # doesn't have we need to either find existing one or create a brand new + # stage. + # + class EnsureStageService < BaseService + def execute(build) + @build = build + + return if build.stage_id.present? + return if build.invalid? + + ensure_stage.tap do |stage| + build.stage_id = stage.id + + yield stage if block_given? + end + end + + private + + def ensure_stage + find_stage || create_stage + end + + def find_stage + @build.pipeline.stages.find_by(name: @build.stage) + end + + def create_stage + Ci::Stage.create!(name: @build.stage, + pipeline: @build.pipeline, + project: @build.project) + end + end +end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index d8e5b55bb88..1f8ae463d0f 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -29,7 +29,7 @@ .form-group = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') - = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' + = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2' .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index ff17372fdd9..3f4415f9e5e 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -67,7 +67,7 @@ - if @commit.last_pipeline - last_pipeline = @commit.last_pipeline .well-segment.pipeline-info - .status-icon-container{ class: "ci-status-icon-#{@commit.status}" } + .status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" } = link_to project_pipeline_path(@project, last_pipeline.id) do = ci_icon_for_status(last_pipeline.status) #{ _('Pipeline') } diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 13809da6523..0d39edb7bfd 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -3,8 +3,8 @@ - if @can_bulk_update = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = link_to "New issue", new_project_issue_path(@project, - issue: { assignee_id: issues_finder.assignee.try(:id), - milestone_id: issues_finder.milestones.first.try(:id) }), + issue: { assignee_id: finder.assignee.try(:id), + milestone_id: finder.milestones.first.try(:id) }), class: "btn btn-new", title: "New issue", id: "new_issue_link" diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index d3f0aa2d339..8442d7ff4a2 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,4 +1,3 @@ -- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder - boards_page = controller.controller_name == 'boards' .issues-filters diff --git a/changelogs/unreleased/38394-smarter-interval.yml b/changelogs/unreleased/38394-smarter-interval.yml new file mode 100644 index 00000000000..ead8c3eca5a --- /dev/null +++ b/changelogs/unreleased/38394-smarter-interval.yml @@ -0,0 +1,5 @@ +--- +title: Update Merge Request polling so there is only one request at a time +merge_request: 15032 +author: +type: fixed diff --git a/changelogs/unreleased/39649-change-default-size-for-gke-cluster-creation.yml b/changelogs/unreleased/39649-change-default-size-for-gke-cluster-creation.yml new file mode 100644 index 00000000000..6faa30177ad --- /dev/null +++ b/changelogs/unreleased/39649-change-default-size-for-gke-cluster-creation.yml @@ -0,0 +1,5 @@ +--- +title: Change default cluster size to n1-default-2 +merge_request: 39649 +author: Fabio Busatto +type: changed diff --git a/changelogs/unreleased/39878-commit-pipeline-reads-wrong-key.yml b/changelogs/unreleased/39878-commit-pipeline-reads-wrong-key.yml new file mode 100644 index 00000000000..b24edfe0cb9 --- /dev/null +++ b/changelogs/unreleased/39878-commit-pipeline-reads-wrong-key.yml @@ -0,0 +1,5 @@ +--- +title: Fix commit pipeline showing wrong status +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-import-export-arguments.yml b/changelogs/unreleased/fix-import-export-arguments.yml new file mode 100644 index 00000000000..eee87e313ea --- /dev/null +++ b/changelogs/unreleased/fix-import-export-arguments.yml @@ -0,0 +1,5 @@ +--- +title: Fix arguments Import/Export error importing project merge requests +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-mysql-grant-check.yml b/changelogs/unreleased/fix-mysql-grant-check.yml new file mode 100644 index 00000000000..a1c1aa67d79 --- /dev/null +++ b/changelogs/unreleased/fix-mysql-grant-check.yml @@ -0,0 +1,5 @@ +--- +title: Fix TRIGGER checks for MySQL +merge_request: +author: +type: fixed diff --git a/db/schema.rb b/db/schema.rb index 3bb2543f044..d8f44d1dcf1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171106101200) do +ActiveRecord::Schema.define(version: 20171101134435) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 96a5a23ee13..8c2690ec3b2 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -166,12 +166,26 @@ board itself. ![Remove issue from list](img/issue_boards_remove_issue.png) -## Re-ordering an issue in a list - -> Introduced in GitLab 9.0. - -Issues can be re-ordered inside of lists. This is as simple as dragging and dropping -an issue into the order you want. +## Issue ordering in a list + +When visiting a board, issues appear ordered in any list. You are able to change +that order simply by dragging and dropping the issues. The changed order will be saved +to the system so that anybody who visits the same board later will see the reordering, +with some exceptions. + +The first time a given issue appears in any board (i.e. the first time a user +loads a board containing that issue), it will be ordered with +respect to other issues in that list according to [Priority order][label-priority]. +At that point, that issue will be assigned a relative order value by the system +representing its relative order with respect to the other issues in the list. Any time +you drag-and-drop reorder that issue, its relative order value will change accordingly. +Also, any time that issue appears in any board when it is loaded by a user, +the updated relative order value will be used for the ordering. (It's only the first +time an issue appears that it takes from the Priority order mentioned above.) This means that +if issue `A` is drag-and-drop reordered to be above issue `B` by any user in +a given board inside your GitLab instance, any time those two issues are subsequently +loaded in any board in the same instance (could be a different project board or a different group board, for example), +that ordering will be maintained. ## Filtering issues diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb index d71e63e73eb..dc90f398c7e 100644 --- a/lib/gitlab/ci/status/build/failed_allowed.rb +++ b/lib/gitlab/ci/status/build/failed_allowed.rb @@ -8,7 +8,7 @@ module Gitlab end def icon - 'warning' + 'status_warning' end def group diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb index aee3981e79a..9f76967fc77 100644 --- a/lib/gitlab/database/grant.rb +++ b/lib/gitlab/database/grant.rb @@ -6,28 +6,36 @@ module Gitlab if Database.postgresql? 'information_schema.role_table_grants' else - 'mysql.user' + 'information_schema.schema_privileges' end - def self.scope_to_current_user - if Database.postgresql? - where('grantee = user') - else - where("CONCAT(User, '@', Host) = current_user()") - end - end - # Returns true if the current user can create and execute triggers on the # given table. def self.create_and_execute_trigger?(table) priv = if Database.postgresql? where(privilege_type: 'TRIGGER', table_name: table) + .where('grantee = user') else - where(Trigger_priv: 'Y') + queries = [ + Grant.select(1) + .from('information_schema.user_privileges') + .where("PRIVILEGE_TYPE = 'SUPER'") + .where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')"), + + Grant.select(1) + .from('information_schema.schema_privileges') + .where("PRIVILEGE_TYPE = 'TRIGGER'") + .where('TABLE_SCHEMA = ?', Gitlab::Database.database_name) + .where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')") + ] + + union = SQL::Union.new(queries).to_sql + + Grant.from("(#{union}) privs") end - priv.scope_to_current_user.any? + priv.any? end end end diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb index 81a213e8321..61db4bd9ccc 100644 --- a/lib/gitlab/import_export/merge_request_parser.rb +++ b/lib/gitlab/import_export/merge_request_parser.rb @@ -26,7 +26,7 @@ module Gitlab end def fetch_ref - @project.repository.fetch_ref(@project.repository.path, @diff_head_sha, @merge_request.source_branch) + @project.repository.fetch_ref(@project.repository, source_ref: @diff_head_sha, target_ref: @merge_request.source_branch) end def branch_exists?(branch_name) diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb index 17c6317f32f..c8479fbc0e8 100644 --- a/lib/gitlab/kubernetes/namespace.rb +++ b/lib/gitlab/kubernetes/namespace.rb @@ -4,7 +4,7 @@ module Gitlab attr_accessor :name def initialize(name, client) - self.name = name + @name = name @client = client end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 1650263b98d..9dcf44fdc3e 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -33,24 +33,29 @@ namespace :gitlab do backup.unpack unless backup.skipped?('db') - unless ENV['force'] == 'yes' - warning = <<-MSG.strip_heredoc - Before restoring the database we recommend removing all existing - tables to avoid future upgrade problems. Be aware that if you have - custom tables in the GitLab database these tables and all data will be - removed. - MSG - puts warning.color(:red) - ask_to_continue - puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) - sleep(5) + begin + unless ENV['force'] == 'yes' + warning = <<-MSG.strip_heredoc + Before restoring the database, we will remove all existing + tables to avoid future upgrade problems. Be aware that if you have + custom tables in the GitLab database these tables and all data will be + removed. + MSG + puts warning.color(:red) + ask_to_continue + puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) + sleep(5) + end + # Drop all tables Load the schema to ensure we don't have any newer tables + # hanging out from a failed upgrade + $progress.puts 'Cleaning the database ... '.color(:blue) + Rake::Task['gitlab:db:drop_tables'].invoke + $progress.puts 'done'.color(:green) + Rake::Task['gitlab:backup:db:restore'].invoke + rescue Gitlab::TaskAbortedByUserError + puts "Quitting...".color(:red) + exit 1 end - # Drop all tables Load the schema to ensure we don't have any newer tables - # hanging out from a failed upgrade - $progress.puts 'Cleaning the database ... '.color(:blue) - Rake::Task['gitlab:db:drop_tables'].invoke - $progress.puts 'done'.color(:green) - Rake::Task['gitlab:backup:db:restore'].invoke end Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories') diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index c9687af4dd2..cd3bf785d34 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -17,60 +17,6 @@ describe IssuableCollections do controller end - describe '#redirect_out_of_range' do - before do - allow(controller).to receive(:url_for) - end - - it 'returns true and redirects if the offset is out of range' do - relation = double(:relation, current_page: 10) - - expect(controller).to receive(:redirect_to) - expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(true) - end - - it 'returns false if the offset is not out of range' do - relation = double(:relation, current_page: 1) - - expect(controller).not_to receive(:redirect_to) - expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(false) - end - end - - describe '#issues_page_count' do - it 'returns the number of issue pages' do - project = create(:project, :public) - - create(:issue, project: project) - - finder = IssuesFinder.new(user) - issues = finder.execute - - allow(controller).to receive(:issues_finder) - .and_return(finder) - - expect(controller.send(:issues_page_count, issues)).to eq(1) - end - end - - describe '#merge_requests_page_count' do - it 'returns the number of merge request pages' do - project = create(:project, :public) - - create(:merge_request, source_project: project, target_project: project) - - finder = MergeRequestsFinder.new(user) - merge_requests = finder.execute - - allow(controller).to receive(:merge_requests_finder) - .and_return(finder) - - pages = controller.send(:merge_requests_page_count, merge_requests) - - expect(pages).to eq(1) - end - end - describe '#page_count_for_relation' do it 'returns the number of pages' do relation = double(:relation, limit_value: 20) diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb index 3a3e7467ef2..748ae040928 100644 --- a/spec/controllers/projects/refs_controller_spec.rb +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -23,12 +23,15 @@ describe Projects::RefsController do xhr :get, :logs_tree, namespace_id: project.namespace.to_param, - project_id: project, id: 'master', - path: 'foo/bar/baz.html', format: format + project_id: project, + id: 'master', + path: 'foo/bar/baz.html', + format: format end it 'never throws MissingTemplate' do expect { default_get }.not_to raise_error + expect { xhr_get(:json) }.not_to raise_error expect { xhr_get }.not_to raise_error end @@ -42,5 +45,12 @@ describe Projects::RefsController do xhr_get(:js) expect(response).to be_success end + + it 'renders JSON' do + xhr_get(:json) + + expect(response).to be_success + expect(json_response).to be_kind_of(Array) + end end end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 169590deb8e..abbe37df90e 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :commit_status, class: CommitStatus do name 'default' + stage 'test' status 'success' description 'commit status' pipeline factory: :ci_pipeline_with_one_job diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb index 807a2189cc4..91282063a8d 100644 --- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb +++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb @@ -12,6 +12,13 @@ feature 'Mini Pipeline Graph in Commit View', :js do end let(:build) { create(:ci_build, pipeline: pipeline) } + it 'display icon with status' do + build.run + visit project_commit_path(project, project.commit.id) + + expect(page).to have_selector('.ci-status-icon-running') + end + it 'displays a mini pipeline graph' do build.run visit project_commit_path(project, project.commit.id) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 9614c72cdc3..fb6a3b8e733 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 8ee7b9cf015..1686e7fa342 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -31,10 +31,5 @@ feature 'Multi-file editor new directory', :js do click_button('Commit 1 file') expect(page).to have_selector('td', text: 'commit message') - - click_link('foldername') - - expect(page).to have_selector('td', text: 'commit message', count: 2) - expect(page).to have_selector('td', text: '.gitkeep') end end diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index c45f8a18d1f..bf9181fb09c 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -20,7 +20,7 @@ describe('RepoFile', () => { resetStore(vm.$store); }); - it('renders link, icon, name and last commit details', () => { + it('renders link, icon and name', () => { const RepoFile = Vue.extend(repoFile); vm = new RepoFile({ store, @@ -37,10 +37,9 @@ describe('RepoFile', () => { expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px'); expect(name.href).toMatch(`/${vm.file.url}`); expect(name.textContent.trim()).toEqual(vm.file.name); - expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(vm.file.lastCommit.message); - expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated); expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy(); expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`); + expect(vm.$el.querySelectorAll('.animation-container').length).toBe(2); }); it('does render if hasFiles is true and is loading tree', () => { diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 7833bf3fb04..1c87fcec245 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,6 +1,6 @@ -import '~/smart_interval'; +import SmartInterval from '~/smart_interval'; -(() => { +describe('SmartInterval', function () { const DEFAULT_MAX_INTERVAL = 100; const DEFAULT_STARTING_INTERVAL = 5; const DEFAULT_SHORT_TIMEOUT = 75; @@ -9,7 +9,7 @@ import '~/smart_interval'; function createDefaultSmartInterval(config) { const defaultParams = { - callback: () => {}, + callback: () => Promise.resolve(), startingInterval: DEFAULT_STARTING_INTERVAL, maxInterval: DEFAULT_MAX_INTERVAL, incrementByFactorOf: DEFAULT_INCREMENT_FACTOR, @@ -22,158 +22,171 @@ import '~/smart_interval'; _.extend(defaultParams, config); } - return new gl.SmartInterval(defaultParams); + return new SmartInterval(defaultParams); } - describe('SmartInterval', function () { - describe('Increment Interval', function () { - beforeEach(function () { - this.smartInterval = createDefaultSmartInterval(); - }); + describe('Increment Interval', function () { + beforeEach(function () { + this.smartInterval = createDefaultSmartInterval(); + }); - it('should increment the interval delay', function (done) { - const interval = this.smartInterval; - setTimeout(() => { - const intervalConfig = this.smartInterval.cfg; - const iterationCount = 4; - const maxIntervalAfterIterations = intervalConfig.startingInterval * - (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40 - const currentInterval = interval.getCurrentInterval(); - - // Provide some flexibility for performance of testing environment - expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval); - expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy(); - - done(); - }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40) - }); + it('should increment the interval delay', function (done) { + const interval = this.smartInterval; + setTimeout(() => { + const intervalConfig = this.smartInterval.cfg; + const iterationCount = 4; + const maxIntervalAfterIterations = intervalConfig.startingInterval * + (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40 + const currentInterval = interval.getCurrentInterval(); + + // Provide some flexibility for performance of testing environment + expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval); + expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy(); + + done(); + }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40) + }); - it('should not increment past maxInterval', function (done) { - const interval = this.smartInterval; + it('should not increment past maxInterval', function (done) { + const interval = this.smartInterval; - setTimeout(() => { - const currentInterval = interval.getCurrentInterval(); - expect(currentInterval).toBe(interval.cfg.maxInterval); + setTimeout(() => { + const currentInterval = interval.getCurrentInterval(); + expect(currentInterval).toBe(interval.cfg.maxInterval); - done(); - }, DEFAULT_LONG_TIMEOUT); - }); + done(); + }, DEFAULT_LONG_TIMEOUT); }); - describe('Public methods', function () { - beforeEach(function () { - this.smartInterval = createDefaultSmartInterval(); + it('does not increment while waiting for callback', function () { + jasmine.clock().install(); + + const smartInterval = createDefaultSmartInterval({ + callback: () => new Promise($.noop), }); - it('should cancel an interval', function (done) { - const interval = this.smartInterval; + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); + + const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR; + expect(smartInterval.getCurrentInterval()).toEqual(oneInterval); - setTimeout(() => { - interval.cancel(); + jasmine.clock().uninstall(); + }); + }); - const intervalId = interval.state.intervalId; - const currentInterval = interval.getCurrentInterval(); - const intervalLowerLimit = interval.cfg.startingInterval; + describe('Public methods', function () { + beforeEach(function () { + this.smartInterval = createDefaultSmartInterval(); + }); - expect(intervalId).toBeUndefined(); - expect(currentInterval).toBe(intervalLowerLimit); + it('should cancel an interval', function (done) { + const interval = this.smartInterval; - done(); - }, DEFAULT_SHORT_TIMEOUT); - }); + setTimeout(() => { + interval.cancel(); - it('should resume an interval', function (done) { - const interval = this.smartInterval; + const intervalId = interval.state.intervalId; + const currentInterval = interval.getCurrentInterval(); + const intervalLowerLimit = interval.cfg.startingInterval; - setTimeout(() => { - interval.cancel(); + expect(intervalId).toBeUndefined(); + expect(currentInterval).toBe(intervalLowerLimit); - interval.resume(); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); - const intervalId = interval.state.intervalId; + it('should resume an interval', function (done) { + const interval = this.smartInterval; - expect(intervalId).toBeTruthy(); + setTimeout(() => { + interval.cancel(); - done(); - }, DEFAULT_SHORT_TIMEOUT); - }); + interval.resume(); + + const intervalId = interval.state.intervalId; + + expect(intervalId).toBeTruthy(); + + done(); + }, DEFAULT_SHORT_TIMEOUT); }); + }); - describe('DOM Events', function () { - beforeEach(function () { - // This ensures DOM and DOM events are initialized for these specs. - setFixtures('<div></div>'); + describe('DOM Events', function () { + beforeEach(function () { + // This ensures DOM and DOM events are initialized for these specs. + setFixtures('<div></div>'); - this.smartInterval = createDefaultSmartInterval(); - }); + this.smartInterval = createDefaultSmartInterval(); + }); - it('should pause when page is not visible', function (done) { - const interval = this.smartInterval; + it('should pause when page is not visible', function (done) { + const interval = this.smartInterval; - setTimeout(() => { - expect(interval.state.intervalId).toBeTruthy(); + setTimeout(() => { + expect(interval.state.intervalId).toBeTruthy(); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); - expect(interval.state.intervalId).toBeUndefined(); - done(); - }, DEFAULT_SHORT_TIMEOUT); - }); + expect(interval.state.intervalId).toBeUndefined(); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); - it('should change to the hidden interval when page is not visible', function (done) { - const HIDDEN_INTERVAL = 1500; - const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); + it('should change to the hidden interval when page is not visible', function (done) { + const HIDDEN_INTERVAL = 1500; + const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); - setTimeout(() => { - expect(interval.state.intervalId).toBeTruthy(); - expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && - interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); + setTimeout(() => { + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && + interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); - expect(interval.state.intervalId).toBeTruthy(); - expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); - done(); - }, DEFAULT_SHORT_TIMEOUT); - }); + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); - it('should resume when page is becomes visible at the previous interval', function (done) { - const interval = this.smartInterval; + it('should resume when page is becomes visible at the previous interval', function (done) { + const interval = this.smartInterval; - setTimeout(() => { - expect(interval.state.intervalId).toBeTruthy(); + setTimeout(() => { + expect(interval.state.intervalId).toBeTruthy(); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); - expect(interval.state.intervalId).toBeUndefined(); + expect(interval.state.intervalId).toBeUndefined(); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); - expect(interval.state.intervalId).toBeTruthy(); + expect(interval.state.intervalId).toBeTruthy(); - done(); - }, DEFAULT_SHORT_TIMEOUT); - }); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); - it('should cancel on page unload', function (done) { - const interval = this.smartInterval; + it('should cancel on page unload', function (done) { + const interval = this.smartInterval; - setTimeout(() => { - $(document).triggerHandler('beforeunload'); - expect(interval.state.intervalId).toBeUndefined(); - expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); - done(); - }, DEFAULT_SHORT_TIMEOUT); - }); + setTimeout(() => { + $(document).triggerHandler('beforeunload'); + expect(interval.state.intervalId).toBeUndefined(); + expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); - it('should execute callback before first interval', function () { - const interval = createDefaultSmartInterval({ immediateExecution: true }); - expect(interval.cfg.immediateExecution).toBeFalsy(); - }); + it('should execute callback before first interval', function () { + const interval = createDefaultSmartInterval({ immediateExecution: true }); + expect(interval.cfg.immediateExecution).toBeFalsy(); }); }); -})(window.gl || (window.gl = {})); +}); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index e4324e91502..ba3721e7c84 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -121,24 +121,28 @@ describe('mrWidgetOptions', () => { describe('initPolling', () => { it('should call SmartInterval', () => { - spyOn(gl, 'SmartInterval').and.returnValue({ - resume() {}, - stopTimer() {}, - }); + spyOn(vm, 'checkStatus').and.returnValue(Promise.resolve()); + jasmine.clock().install(); vm.initPolling(); + expect(vm.checkStatus).not.toHaveBeenCalled(); + + jasmine.clock().tick(10000); + expect(vm.pollingInterval).toBeDefined(); - expect(gl.SmartInterval).toHaveBeenCalled(); + expect(vm.checkStatus).toHaveBeenCalled(); + + jasmine.clock().uninstall(); }); }); describe('initDeploymentsPolling', () => { it('should call SmartInterval', () => { - spyOn(gl, 'SmartInterval'); + spyOn(vm, 'fetchDeployments').and.returnValue(Promise.resolve()); vm.initDeploymentsPolling(); expect(vm.deploymentsInterval).toBeDefined(); - expect(gl.SmartInterval).toHaveBeenCalled(); + expect(vm.fetchDeployments).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js new file mode 100644 index 00000000000..a5db0b2c59e --- /dev/null +++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Skeleton loading container', () => { + let vm; + + beforeEach(() => { + const component = Vue.extend(skeletonLoadingContainer); + vm = mountComponent(component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders 6 skeleton lines by default', () => { + expect(vm.$el.querySelector('.skeleton-line-6')).not.toBeNull(); + }); + + it('renders in full mode by default', () => { + expect(vm.$el.classList.contains('animation-container-small')).toBeFalsy(); + }); + + describe('small', () => { + beforeEach((done) => { + vm.small = true; + + Vue.nextTick(done); + }); + + it('renders in small mode', () => { + expect(vm.$el.classList.contains('animation-container-small')).toBeTruthy(); + }); + }); + + describe('lines', () => { + beforeEach((done) => { + vm.lines = 5; + + Vue.nextTick(done); + }); + + it('renders 5 lines', () => { + expect(vm.$el.querySelector('.skeleton-line-5')).not.toBeNull(); + expect(vm.$el.querySelector('.skeleton-line-6')).toBeNull(); + }); + }); +}); diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 2b32e47e9ba..d196bc6a4c2 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -84,7 +84,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' - expect(status.icon).to eq 'warning' + expect(status.icon).to eq 'status_warning' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed (allowed to fail)' expect(status).to have_details diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb index 79a65fc67e8..99a5a7e4aca 100644 --- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do describe '#icon' do it 'returns a warning icon' do - expect(subject.icon).to eq 'warning' + expect(subject.icon).to eq 'status_warning' end end diff --git a/spec/lib/gitlab/database/grant_spec.rb b/spec/lib/gitlab/database/grant_spec.rb index 651da3e8476..5ebf3f399b6 100644 --- a/spec/lib/gitlab/database/grant_spec.rb +++ b/spec/lib/gitlab/database/grant_spec.rb @@ -1,16 +1,6 @@ require 'spec_helper' describe Gitlab::Database::Grant do - describe '.scope_to_current_user' do - it 'scopes the relation to the current user' do - user = Gitlab::Database.username - column = Gitlab::Database.postgresql? ? :grantee : :User - names = described_class.scope_to_current_user.pluck(column).uniq - - expect(names).to eq([user]) - end - end - describe '.create_and_execute_trigger' do it 'returns true when the user can create and execute a trigger' do # We assume the DB/user is set up correctly so that triggers can be @@ -18,13 +8,11 @@ describe Gitlab::Database::Grant do expect(described_class.create_and_execute_trigger?('users')).to eq(true) end - it 'returns false when the user can not create and/or execute a trigger' do - allow(described_class).to receive(:scope_to_current_user) - .and_return(described_class.none) - - result = described_class.create_and_execute_trigger?('kittens') - - expect(result).to eq(false) + it 'returns false when the user can not create and/or execute a trigger', :postgresql do + # In case of MySQL the user may have SUPER permissions, making it + # impossible to have `false` returned when running tests; hence we only + # run these tests on PostgreSQL. + expect(described_class.create_and_execute_trigger?('foo')).to eq(false) end end end diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 473ba40fae7..b793636c4d6 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::ImportExport::MergeRequestParser do let(:parsed_merge_request) do described_class.new(project, - merge_request.diff_head_sha, + 'abcd', merge_request, merge_request.as_json).parse! end @@ -29,4 +29,14 @@ describe Gitlab::ImportExport::MergeRequestParser do it 'has a target branch' do expect(project.repository.branch_exists?(parsed_merge_request.target_branch)).to be true end + + it 'parses a MR that has no source branch' do + allow_any_instance_of(described_class).to receive(:branch_exists?).and_call_original + allow_any_instance_of(described_class).to receive(:branch_exists?).with(merge_request.source_branch).and_return(false) + allow_any_instance_of(described_class).to receive(:fork_merge_request?).and_return(true) + allow(Gitlab::GitalyClient).to receive(:migrate).and_call_original + allow(Gitlab::GitalyClient).to receive(:migrate).with(:fetch_ref).and_return([nil, 0]) + + expect(parsed_merge_request).to eq(merge_request) + end end diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index acc5bd1da35..fac23dce44d 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -69,7 +69,7 @@ describe GoogleApi::CloudPlatform::Client do let(:cluster_name) { 'test-cluster' } let(:cluster_size) { 1 } - let(:machine_type) { 'n1-standard-4' } + let(:machine_type) { 'n1-standard-2' } let(:operation) { double } before do diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb index ecd0a08c953..b38b5e6bcad 100644 --- a/spec/models/clusters/providers/gcp_spec.rb +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -10,7 +10,7 @@ describe Clusters::Providers::Gcp do it "has default value" do expect(gcp.zone).to eq('us-central1-a') expect(gcp.num_nodes).to eq(3) - expect(gcp.machine_type).to eq('n1-standard-4') + expect(gcp.machine_type).to eq('n1-standard-2') end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 858ec831200..c536dab2681 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe CommitStatus do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } - let(:pipeline) do + set(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id) end @@ -464,4 +464,73 @@ describe CommitStatus do it { is_expected.to be_script_failure } end end + + describe 'ensure stage assignment' do + context 'when commit status has a stage_id assigned' do + let!(:stage) do + create(:ci_stage_entity, project: project, pipeline: pipeline) + end + + let(:commit_status) do + create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test') + end + + it 'does not create a new stage' do + expect { commit_status }.not_to change { Ci::Stage.count } + expect(commit_status.stage_id).to eq stage.id + end + end + + context 'when commit status does not have a stage_id assigned' do + let(:commit_status) do + create(:commit_status, name: 'rspec', stage: 'test', status: :success) + end + + let(:stage) { Ci::Stage.first } + + it 'creates a new stage' do + expect { commit_status }.to change { Ci::Stage.count }.by(1) + + expect(stage.name).to eq 'test' + expect(stage.project).to eq commit_status.project + expect(stage.pipeline).to eq commit_status.pipeline + expect(stage.status).to eq commit_status.status + expect(commit_status.stage_id).to eq stage.id + end + end + + context 'when commit status does not have stage but it exists' do + let!(:stage) do + create(:ci_stage_entity, project: project, + pipeline: pipeline, + name: 'test') + end + + let(:commit_status) do + create(:commit_status, project: project, + pipeline: pipeline, + name: 'rspec', + stage: 'test', + status: :success) + end + + it 'uses existing stage' do + expect { commit_status }.not_to change { Ci::Stage.count } + + expect(commit_status.stage_id).to eq stage.id + expect(stage.reload.status).to eq commit_status.status + end + end + + context 'when commit status is being imported' do + let(:commit_status) do + create(:commit_status, name: 'rspec', stage: 'test', importing: true) + end + + it 'does not create a new stage' do + expect { commit_status }.not_to change { Ci::Stage.count } + expect(commit_status.stage_id).not_to be_present + end + end + end end diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index f60d1843581..45e18086894 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -107,7 +107,7 @@ describe PipelineDetailsEntity do it 'contains stages' do expect(subject).to include(:details) expect(subject[:details]).to include(:stages) - expect(subject[:details][:stages].first).to include(name: 'external') + expect(subject[:details][:stages].first).to include(name: 'test') end end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 934b4557ba2..26fd271ce31 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -94,6 +94,7 @@ module CycleAnalyticsHelpers ref: 'master', tag: false, name: 'dummy', + stage: 'dummy', pipeline: dummy_pipeline, protected: false) end |