Merge branch 'master' into 38464-k8s-apps

This commit is contained in:
Shinya Maeda 2017-11-08 01:46:53 +09:00
commit 4fac95a64d
66 changed files with 791 additions and 467 deletions

View File

@ -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>

View File

@ -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>

View File

@ -80,7 +80,7 @@ export default {
/>
<repo-file
v-for="(file, index) in treeList"
:key="index"
:key="file.key"
:file="file"
/>
</tbody>

View File

@ -30,4 +30,11 @@ export default {
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
getTreeLastCommit(endpoint) {
return Vue.http.get(endpoint, {
params: {
format: 'json',
},
});
},
};

View File

@ -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');

View File

@ -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) => {

View 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 });
};

View File

@ -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';

View File

@ -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,

View File

@ -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);
},

View File

@ -8,6 +8,7 @@ export default () => ({
endpoints: {},
isRoot: false,
isInitialRoot: false,
lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],

View File

@ -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,
});
};

View File

@ -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;

View File

@ -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) {

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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') }

View File

@ -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"

View File

@ -1,4 +1,3 @@
- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters

View File

@ -0,0 +1,5 @@
---
title: Update Merge Request polling so there is only one request at a time
merge_request: 15032
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Change default cluster size to n1-default-2
merge_request: 39649
author: Fabio Busatto
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix commit pipeline showing wrong status
merge_request:
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix arguments Import/Export error importing project merge requests
merge_request:
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix TRIGGER checks for MySQL
merge_request:
author:
type: fixed

View File

@ -166,12 +166,26 @@ board itself.
![Remove issue from list](img/issue_boards_remove_issue.png)
## 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

View File

@ -8,7 +8,7 @@ module Gitlab
end
def icon
'warning'
'status_warning'
end
def group

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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', () => {

View File

@ -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();
});
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -94,6 +94,7 @@ module CycleAnalyticsHelpers
ref: 'master',
tag: false,
name: 'dummy',
stage: 'dummy',
pipeline: dummy_pipeline,
protected: false)
end