Merge branch 'master' into 38464-k8s-apps
This commit is contained in:
commit
4fac95a64d
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default {
|
|||
/>
|
||||
<repo-file
|
||||
v-for="(file, index) in treeList"
|
||||
:key="index"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
/>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -30,4 +30,11 @@ export default {
|
|||
commit(projectId, payload) {
|
||||
return Api.commitMultiple(projectId, payload);
|
||||
},
|
||||
getTreeLastCommit(endpoint) {
|
||||
return Vue.http.get(endpoint, {
|
||||
params: {
|
||||
format: 'json',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default () => ({
|
|||
endpoints: {},
|
||||
isRoot: false,
|
||||
isInitialRoot: false,
|
||||
lastCommitPath: '',
|
||||
loading: false,
|
||||
onTopOfBranch: false,
|
||||
openFiles: [],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
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
|
||||
end
|
||||
|
||||
def issues_collection
|
||||
issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
|
||||
def issuables_collection
|
||||
finder.execute.preload(preload_for_collection)
|
||||
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
|
||||
|
||||
def issues_finder
|
||||
@issues_finder ||= issuable_finder_for(IssuesFinder)
|
||||
end
|
||||
|
||||
def merge_requests_finder
|
||||
@merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
|
||||
- boards_page = controller.controller_name == 'boards'
|
||||
|
||||
.issues-filters
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update Merge Request polling so there is only one request at a time
|
||||
merge_request: 15032
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change default cluster size to n1-default-2
|
||||
merge_request: 39649
|
||||
author: Fabio Busatto
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix commit pipeline showing wrong status
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix arguments Import/Export error importing project merge requests
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix TRIGGER checks for MySQL
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -166,12 +166,26 @@ board itself.
|
|||
|
||||

|
||||
|
||||
## Re-ordering an issue in a list
|
||||
## Issue ordering in a list
|
||||
|
||||
> Introduced in GitLab 9.0.
|
||||
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.
|
||||
|
||||
Issues can be re-ordered inside of lists. This is as simple as dragging and dropping
|
||||
an issue into the order you want.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def icon
|
||||
'warning'
|
||||
'status_warning'
|
||||
end
|
||||
|
||||
def group
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
setTimeout(() => {
|
||||
const currentInterval = interval.getCurrentInterval();
|
||||
expect(currentInterval).toBe(interval.cfg.maxInterval);
|
||||
|
||||
done();
|
||||
}, DEFAULT_LONG_TIMEOUT);
|
||||
});
|
||||
describe('Increment Interval', function () {
|
||||
beforeEach(function () {
|
||||
this.smartInterval = createDefaultSmartInterval();
|
||||
});
|
||||
|
||||
describe('Public methods', 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();
|
||||
|
||||
it('should cancel an interval', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
// Provide some flexibility for performance of testing environment
|
||||
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
|
||||
expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
|
||||
|
||||
setTimeout(() => {
|
||||
interval.cancel();
|
||||
|
||||
const intervalId = interval.state.intervalId;
|
||||
const currentInterval = interval.getCurrentInterval();
|
||||
const intervalLowerLimit = interval.cfg.startingInterval;
|
||||
|
||||
expect(intervalId).toBeUndefined();
|
||||
expect(currentInterval).toBe(intervalLowerLimit);
|
||||
|
||||
done();
|
||||
}, DEFAULT_SHORT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('should resume an interval', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
|
||||
setTimeout(() => {
|
||||
interval.cancel();
|
||||
|
||||
interval.resume();
|
||||
|
||||
const intervalId = interval.state.intervalId;
|
||||
|
||||
expect(intervalId).toBeTruthy();
|
||||
|
||||
done();
|
||||
}, DEFAULT_SHORT_TIMEOUT);
|
||||
});
|
||||
done();
|
||||
}, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
|
||||
});
|
||||
|
||||
describe('DOM Events', function () {
|
||||
beforeEach(function () {
|
||||
// This ensures DOM and DOM events are initialized for these specs.
|
||||
setFixtures('<div></div>');
|
||||
it('should not increment past maxInterval', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
|
||||
this.smartInterval = createDefaultSmartInterval();
|
||||
setTimeout(() => {
|
||||
const currentInterval = interval.getCurrentInterval();
|
||||
expect(currentInterval).toBe(interval.cfg.maxInterval);
|
||||
|
||||
done();
|
||||
}, DEFAULT_LONG_TIMEOUT);
|
||||
});
|
||||
|
||||
it('does not increment while waiting for callback', function () {
|
||||
jasmine.clock().install();
|
||||
|
||||
const smartInterval = createDefaultSmartInterval({
|
||||
callback: () => new Promise($.noop),
|
||||
});
|
||||
|
||||
it('should pause when page is not visible', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(interval.state.intervalId).toBeTruthy();
|
||||
const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
|
||||
expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
|
||||
|
||||
// simulates triggering of visibilitychange event
|
||||
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
|
||||
|
||||
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 });
|
||||
|
||||
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' } });
|
||||
|
||||
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;
|
||||
|
||||
setTimeout(() => {
|
||||
expect(interval.state.intervalId).toBeTruthy();
|
||||
|
||||
// simulates triggering of visibilitychange event
|
||||
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
|
||||
|
||||
expect(interval.state.intervalId).toBeUndefined();
|
||||
|
||||
// simulates triggering of visibilitychange event
|
||||
interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
|
||||
|
||||
expect(interval.state.intervalId).toBeTruthy();
|
||||
|
||||
done();
|
||||
}, DEFAULT_SHORT_TIMEOUT);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('should execute callback before first interval', function () {
|
||||
const interval = createDefaultSmartInterval({ immediateExecution: true });
|
||||
expect(interval.cfg.immediateExecution).toBeFalsy();
|
||||
});
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
||||
describe('Public methods', function () {
|
||||
beforeEach(function () {
|
||||
this.smartInterval = createDefaultSmartInterval();
|
||||
});
|
||||
|
||||
it('should cancel an interval', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
|
||||
setTimeout(() => {
|
||||
interval.cancel();
|
||||
|
||||
const intervalId = interval.state.intervalId;
|
||||
const currentInterval = interval.getCurrentInterval();
|
||||
const intervalLowerLimit = interval.cfg.startingInterval;
|
||||
|
||||
expect(intervalId).toBeUndefined();
|
||||
expect(currentInterval).toBe(intervalLowerLimit);
|
||||
|
||||
done();
|
||||
}, DEFAULT_SHORT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('should resume an interval', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
|
||||
setTimeout(() => {
|
||||
interval.cancel();
|
||||
|
||||
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>');
|
||||
|
||||
this.smartInterval = createDefaultSmartInterval();
|
||||
});
|
||||
|
||||
it('should pause when page is not visible', function (done) {
|
||||
const interval = this.smartInterval;
|
||||
|
||||
setTimeout(() => {
|
||||
expect(interval.state.intervalId).toBeTruthy();
|
||||
|
||||
// simulates triggering of visibilitychange event
|
||||
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
|
||||
|
||||
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 });
|
||||
|
||||
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' } });
|
||||
|
||||
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;
|
||||
|
||||
setTimeout(() => {
|
||||
expect(interval.state.intervalId).toBeTruthy();
|
||||
|
||||
// simulates triggering of visibilitychange event
|
||||
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
|
||||
|
||||
expect(interval.state.intervalId).toBeUndefined();
|
||||
|
||||
// simulates triggering of visibilitychange event
|
||||
interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
|
||||
|
||||
expect(interval.state.intervalId).toBeTruthy();
|
||||
|
||||
done();
|
||||
}, DEFAULT_SHORT_TIMEOUT);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('should execute callback before first interval', function () {
|
||||
const interval = createDefaultSmartInterval({ immediateExecution: true });
|
||||
expect(interval.cfg.immediateExecution).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ module CycleAnalyticsHelpers
|
|||
ref: 'master',
|
||||
tag: false,
|
||||
name: 'dummy',
|
||||
stage: 'dummy',
|
||||
pipeline: dummy_pipeline,
|
||||
protected: false)
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue