diff options
author | Shinya Maeda <shinya@gitlab.com> | 2017-11-08 01:46:53 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2017-11-08 01:46:53 +0900 |
commit | 4fac95a64d0c04599af9c762edb94414d85bf42f (patch) | |
tree | b87775aff426603984bc547208860ad147fcff69 /app | |
parent | 02878cd958557128cd9c22b27bd2fb97a843266b (diff) | |
parent | 396f45ade1dddec36df9861f8a1bb80aabd2ff15 (diff) | |
download | gitlab-ce-4fac95a64d0c04599af9c762edb94414d85bf42f.tar.gz |
Merge branch 'master' into 38464-k8s-apps
Diffstat (limited to 'app')
37 files changed, 373 insertions, 193 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 |