Commit 3ff95c3c authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-08-13' into 'master'

CE upstream - 2018-08-13 21:22 UTC

Closes gitlab-ce#50210

See merge request gitlab-org/gitlab-ee!6888
parents a81f41c0 c6db62e9
...@@ -839,7 +839,7 @@ GEM ...@@ -839,7 +839,7 @@ GEM
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.2) rugged (0.27.4)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
......
...@@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) { ...@@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) {
canvas.height = numTestEntries * fontSize; canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// Write each emoji to the canvas vertically // Write each emoji to the canvas vertically
let writeIndex = 0; let writeIndex = 0;
testMapKeys.forEach(testKey => { testMapKeys.forEach(testKey => {
......
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
<slot name="header"></slot> <slot name="header"></slot>
</header> </header>
<div <div
class="ide-tree-body" class="ide-tree-body h-100"
> >
<repo-file <repo-file
v-for="file in currentTree.tree" v-for="file in currentTree.tree"
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
directives: {
tooltip,
},
components: { components: {
Icon, Icon,
}, },
...@@ -26,6 +30,11 @@ export default { ...@@ -26,6 +30,11 @@ export default {
default: true, default: true,
}, },
}, },
computed: {
tooltipTitle() {
return this.showLabel ? '' : this.label;
},
},
methods: { methods: {
clicked() { clicked() {
this.$emit('click'); this.$emit('click');
...@@ -36,7 +45,9 @@ export default { ...@@ -36,7 +45,9 @@ export default {
<template> <template>
<button <button
v-tooltip
:aria-label="label" :aria-label="label"
:title="tooltipTitle"
type="button" type="button"
class="btn-blank" class="btn-blank"
@click.stop.prevent="clicked" @click.stop.prevent="clicked"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { Manager } from 'smooshpack'; import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Navigator from './navigator.vue'; import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants'; import { packageJsonPath } from '../../constants';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
return { return {
manager: {}, manager: {},
loading: false, loading: false,
sandpackReady: false,
}; };
}, },
computed: { computed: {
...@@ -81,6 +83,10 @@ export default { ...@@ -81,6 +83,10 @@ export default {
} }
this.manager = {}; this.manager = {};
if (this.listener) {
this.listener();
}
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.timeout = null; this.timeout = null;
}, },
...@@ -96,17 +102,29 @@ export default { ...@@ -96,17 +102,29 @@ export default {
return this.loadFileContent(this.mainEntry) return this.loadFileContent(this.mainEntry)
.then(() => this.$nextTick()) .then(() => this.$nextTick())
.then(() => .then(() => {
this.initManager('#ide-preview', this.sandboxOpts, { this.initManager('#ide-preview', this.sandboxOpts, {
fileResolver: { fileResolver: {
isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]), isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content), readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content),
}, },
}), });
);
this.listener = listen(e => {
switch (e.type) {
case 'done':
this.sandpackReady = true;
break;
default:
break;
}
});
});
}, },
update() { update() {
if (this.timeout) return; if (!this.sandpackReady) return;
clearTimeout(this.timeout);
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
if (_.isEmpty(this.manager)) { if (_.isEmpty(this.manager)) {
...@@ -116,10 +134,7 @@ export default { ...@@ -116,10 +134,7 @@ export default {
} }
this.manager.updatePreview(this.sandboxOpts); this.manager.updatePreview(this.sandboxOpts);
}, 250);
clearTimeout(this.timeout);
this.timeout = null;
}, 500);
}, },
initManager(el, opts, resolver) { initManager(el, opts, resolver) {
this.manager = new Manager(el, opts, resolver); this.manager = new Manager(el, opts, resolver);
......
...@@ -3,7 +3,6 @@ import VueRouter from 'vue-router'; ...@@ -3,7 +3,6 @@ import VueRouter from 'vue-router';
import { join as joinPath } from 'path'; import { join as joinPath } from 'path';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -74,101 +73,22 @@ router.beforeEach((to, from, next) => { ...@@ -74,101 +73,22 @@ router.beforeEach((to, from, next) => {
projectId: to.params.project, projectId: to.params.project,
}) })
.then(() => { .then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`; const basePath = to.params[0] || '';
const projectId = `${to.params.namespace}/${to.params.project}`;
const branchId = to.params.branchid; const branchId = to.params.branchid;
const mergeRequestId = to.params.mrid;
if (branchId) { if (branchId) {
const basePath = to.params[0] || ''; store.dispatch('openBranch', {
projectId,
store.dispatch('setCurrentBranchId', branchId);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId, branchId,
basePath,
}); });
} else if (mergeRequestId) {
store store.dispatch('openMergeRequest', {
.dispatch('getFiles', { projectId,
projectId: fullProjectId, mergeRequestId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
const treeEntry = store.state.entries[treeEntryKey];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
}
})
.catch(e => {
throw e;
});
} else if (to.params.mrid) {
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
targetProjectId: to.query.target_project, targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('setCurrentBranchId', mr.source_branch);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
return store.dispatch('getFiles', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
})
.then(() =>
store.dispatch('getMergeRequestVersions', {
projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid,
}),
)
.then(() =>
store.dispatch('getMergeRequestChanges', {
projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid,
}),
)
.then(mrChanges => {
if (mrChanges.changes.length) {
store.dispatch('updateActivityBarView', activityBarViews.review);
}
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = store.state.entries[change.new_path];
if (changeTreeEntry) {
store.dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
store.dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash('Error while loading the merge request. Please try again.');
throw e;
}); });
} }
}) })
......
import { __ } from '../../../locale'; import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { activityBarViews } from '../../constants';
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, dispatch, state }, { commit, dispatch, state },
...@@ -104,3 +106,67 @@ export const getMergeRequestVersions = ( ...@@ -104,3 +106,67 @@ export const getMergeRequestVersions = (
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions); resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
} }
}); });
export const openMergeRequest = (
{ dispatch, state },
{ projectId, targetProjectId, mergeRequestId } = {},
) =>
dispatch('getMergeRequestData', {
projectId,
targetProjectId,
mergeRequestId,
})
.then(mr => {
dispatch('setCurrentBranchId', mr.source_branch);
dispatch('getBranchData', {
projectId,
branchId: mr.source_branch,
});
return dispatch('getFiles', {
projectId,
branchId: mr.source_branch,
});
})
.then(() =>
dispatch('getMergeRequestVersions', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(() =>
dispatch('getMergeRequestChanges', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(mrChanges => {
if (mrChanges.changes.length) {
dispatch('updateActivityBarView', activityBarViews.review);
}
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = state.entries[change.new_path];
if (changeTreeEntry) {
dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash(__('Error while loading the merge request. Please try again.'));
throw e;
});
...@@ -124,3 +124,35 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { ...@@ -124,3 +124,35 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
actionPayload: branchId, actionPayload: branchId,
}); });
}; };
export const openBranch = (
{ dispatch, state },
{ projectId, branchId, basePath },
) => {
dispatch('setCurrentBranchId', branchId);
dispatch('getBranchData', {
projectId,
branchId,
});
return (
dispatch('getFiles', {
projectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
}
}
})
);
};
...@@ -33,7 +33,4 @@ export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => { ...@@ -33,7 +33,4 @@ export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => {
export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES); export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES);
export const openBranch = ({ rootState, dispatch }, id) =>
dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true });
export default () => {}; export default () => {};
<script> <script>
import detailRow from './sidebar_detail_row.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
export default { export default {
name: 'SidebarDetailsBlock', name: 'SidebarDetailsBlock',
components: { components: {
detailRow, DetailRow,
loadingIcon, LoadingIcon,
Icon,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
...@@ -20,16 +22,16 @@ export default { ...@@ -20,16 +22,16 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
canUserRetry: {
type: Boolean,
required: false,
default: false,
},
runnerHelpUrl: { runnerHelpUrl: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
terminalPath: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
shouldRenderContent() { shouldRenderContent() {
...@@ -92,7 +94,7 @@ export default { ...@@ -92,7 +94,7 @@ export default {
{{ job.name }} {{ job.name }}
</strong> </strong>
<a <a
v-if="canUserRetry" v-if="job.retry_path"
:class="retryButtonClass" :class="retryButtonClass"
:href="job.retry_path" :href="job.retry_path"
data-method="post" data-method="post"
...@@ -100,6 +102,16 @@ export default { ...@@ -100,6 +102,16 @@ export default {
> >
{{ __('Retry') }} {{ __('Retry') }}
</a> </a>
<a
v-if="terminalPath"
:href="terminalPath"
class="js-terminal-link pull-right btn btn-primary
btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }}
<icon name="external-link" />
</a>
<button <button
:aria-label="__('Toggle Sidebar')" :aria-label="__('Toggle Sidebar')"
type="button" type="button"
...@@ -125,7 +137,7 @@ export default { ...@@ -125,7 +137,7 @@ export default {
{{ __('New issue') }} {{ __('New issue') }}
</a> </a>
<a <a
v-if="canUserRetry" v-if="job.retry_path"
:href="job.retry_path" :href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary" class="js-retry-job btn btn-inverted-secondary"
data-method="post" data-method="post"
......
<script>
/**
* Renders Stuck Runners block for job's view.
*/
export default {
props: {
hasNoRunnersForProject: {
type: Boolean,
required: true,
},
tags: {
type: Array,
required: false,
default: () => [],
},
runnersPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="bs-callout bs-callout-warning">
<p
v-if="hasNoRunnersForProject"
class="js-stuck-no-runners"
>
{{ s__(`Job|This job is stuck, because the project
doesn't have any runners online assigned to it.`) }}
</p>
<p
v-else-if="tags.length"
class="js-stuck-with-tags"
>
{{ s__(`This job is stuck, because you don't have
any active runners online with any of these tags assigned to them:`) }}
<span
v-for="(tag, index) in tags"
:key="index"
class="badge badge-primary"
>
{{ tag }}
</span>
</p>
<p
v-else
class="js-stuck-no-active-runner"
>
{{ s__(`This job is stuck, because you don't
have any active runners that can run this job.`) }}
</p>
{{ __("Go to") }}
<a
v-if="runnersPath"
:href="runnersPath"
class="js-runners-path"
>
{{ __("Runners page") }}
</a>
</div>
</template>
...@@ -52,9 +52,9 @@ export default () => { ...@@ -52,9 +52,9 @@ export default () => {
return createElement('details-block', { return createElement('details-block', {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job, job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl, runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
}, },
}); });
}, },
......
...@@ -2,7 +2,7 @@ gl-emoji { ...@@ -2,7 +2,7 @@ gl-emoji {
font-style: normal; font-style: normal;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1.5em; font-size: 1.5em;
line-height: 0.9; line-height: 0.9;
} }
...@@ -385,7 +385,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); ...@@ -385,7 +385,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
/* /*
* Dropdowns * Dropdowns
......
...@@ -755,8 +755,19 @@ ...@@ -755,8 +755,19 @@
} }
.repository-languages-bar { .repository-languages-bar {
height: 6px; height: 8px;
margin-bottom: 8px; margin-bottom: $gl-padding-8;
background-color: $white-light;
border-radius: $border-radius-default;
.progress-bar {
margin-right: 2px;
padding: 0 $gl-padding-4;
&:last-child {
margin-right: 0;
}
}
} }
pre.light-well { pre.light-well {
......
...@@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base ...@@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
before_action :limit_unauthenticated_session_times
before_action :authenticate_sessionless_user! before_action :authenticate_sessionless_user!
before_action :authenticate_user! before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
...@@ -27,6 +26,7 @@ class ApplicationController < ActionController::Base ...@@ -27,6 +26,7 @@ class ApplicationController < ActionController::Base
around_action :set_locale around_action :set_locale
after_action :set_page_title_header, if: :json_request? after_action :set_page_title_header, if: :json_request?
after_action :limit_unauthenticated_session_times
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
......
...@@ -103,6 +103,10 @@ class User < ActiveRecord::Base ...@@ -103,6 +103,10 @@ class User < ActiveRecord::Base
has_many :groups, through: :group_members has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
has_many :owned_or_maintainers_groups,
-> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
source: :group
alias_attribute :masters_groups, :maintainers_groups alias_attribute :masters_groups, :maintainers_groups
# Projects # Projects
...@@ -1004,15 +1008,7 @@ class User < ActiveRecord::Base ...@@ -1004,15 +1008,7 @@ class User < ActiveRecord::Base
end end
def manageable_groups def manageable_groups
union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants
# Update this line to not use raw SQL when migrated to Rails 5.2.
# Either ActiveRecord or Arel constructions are fine.
# This was replaced with the raw SQL construction because of bugs in the arel gem.
# Bugs were fixed in arel 9.0.0 (Rails 5.2).
owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants
end end
def namespaces def namespaces
...@@ -1266,11 +1262,6 @@ class User < ActiveRecord::Base ...@@ -1266,11 +1262,6 @@ class User < ActiveRecord::Base
!terms_accepted? !terms_accepted?
end end
def owned_or_maintainers_groups
union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups])
Group.from("(#{union.to_sql}) namespaces")
end
# @deprecated # @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
......
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container .sidebar-container
.blocks-container .blocks-container
- if can?(current_user, :create_build_terminal, @build) #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.block
= link_to terminal_project_job_path(@project, @build), class: 'terminal-button pull-right btn visible-md-block visible-lg-block', title: 'Terminal' do
Terminal
#js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block .block
......
---
title: Make terminal button more visible
merge_request:
author:
type: changed
---
title: Creates Vvue component for warning block about stuck runners
merge_request:
author:
type: other
---
title: Vendor Auto-DevOps.gitlab-ci.yml with new proxy env vars passed through to
docker
merge_request: 21159
author: kinolaev
type: added
---
title: Add Noto Color Emoji font support
merge_request: 19036
author: Alexander Popov
type: changed
---
title: Get the merge base of two refs through the API
merge_request: 20929
author:
type: added
---
title: Optimize querying User#manageable_groups
merge_request: 21050
author:
type: performance
---
title: Fix pipeline fixture seeder
merge_request: 21088
author:
type: fixed
---
title: Added tooltips to tree list header
merge_request: 21138
author:
type: added
---
title: Bump rugged to 0.27.4 for security fixes
merge_request:
author:
type: security
---
title: Improve visuals of language bar on projects
merge_request: 21006
author:
type: changed
...@@ -41,7 +41,7 @@ class Gitlab::Seeder::Pipelines ...@@ -41,7 +41,7 @@ class Gitlab::Seeder::Pipelines
when: 'manual', status: :skipped }, when: 'manual', status: :skipped },
# notify stage # notify stage
{ name: 'slack', stage: 'notify', when: 'manual', status: :created }, { name: 'slack', stage: 'notify', when: 'manual', status: :success },
] ]
EXTERNAL_JOBS = [ EXTERNAL_JOBS = [
{ name: 'jenkins', stage: 'test', status: :success, { name: 'jenkins', stage: 'test', status: :success,
...@@ -54,18 +54,12 @@ class Gitlab::Seeder::Pipelines ...@@ -54,18 +54,12 @@ class Gitlab::Seeder::Pipelines
def seed! def seed!
pipelines.each do |pipeline| pipelines.each do |pipeline|
begin
BUILDS.each { |opts| build_create!(pipeline, opts) } BUILDS.each { |opts| build_create!(pipeline, opts) }
EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) } EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) }
print '.'
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
pipeline.update_duration pipeline.update_duration
pipeline.update_status pipeline.update_status
end end
end end
end
private private
...@@ -95,7 +89,9 @@ class Gitlab::Seeder::Pipelines ...@@ -95,7 +89,9 @@ class Gitlab::Seeder::Pipelines
branch = merge_request.source_branch branch = merge_request.source_branch
merge_request.commits.last(4).map do |commit| merge_request.commits.last(4).map do |commit|
create_pipeline!(project, branch, commit) create_pipeline!(project, branch, commit).tap do |pipeline|
merge_request.update!(head_pipeline_id: pipeline.id)
end
end end
end end
...@@ -105,7 +101,7 @@ class Gitlab::Seeder::Pipelines ...@@ -105,7 +101,7 @@ class Gitlab::Seeder::Pipelines
end end
def create_pipeline!(project, ref, commit) def create_pipeline!(project, ref, commit)
project.pipelines.create(sha: commit.id, ref: ref, source: :push) project.pipelines.create!(sha: commit.id, ref: ref, source: :push)
end end
def build_create!(pipeline, opts = {}) def build_create!(pipeline, opts = {})
...@@ -118,24 +114,39 @@ class Gitlab::Seeder::Pipelines ...@@ -118,24 +114,39 @@ class Gitlab::Seeder::Pipelines
# block directly to `Ci::Build#create!`. # block directly to `Ci::Build#create!`.
setup_artifacts(build) setup_artifacts(build)
setup_test_reports(build)
setup_build_log(build) setup_build_log(build)
build.project.environments. build.project.environments.
find_or_create_by(name: build.expanded_environment_name) find_or_create_by(name: build.expanded_environment_name)
build.save build.save!
end end
end end
def setup_artifacts(build) def setup_artifacts(build)
return unless %w[build test].include?(build.stage) return unless build.stage == "build"
artifacts_cache_file(artifacts_archive_path) do |file| artifacts_cache_file(artifacts_archive_path) do |file|
build.job_artifacts.build(project: build.project, file_type: :archive, file: file) build.job_artifacts.build(project: build.project, file_type: :archive, file_format: :zip, file: file)
end end
artifacts_cache_file(artifacts_metadata_path) do |file| artifacts_cache_file(artifacts_metadata_path) do |file|
build.job_artifacts.build(project: build.project, file_type: :metadata, file: file) build.job_artifacts.build(project: build.project, file_type: :metadata, file_format: :gzip, file: file)
end
end
def setup_test_reports(build)
return unless build.stage == "test" && build.name == "rspec:osx"
if build.ref == build.project.default_branch
artifacts_cache_file(test_reports_pass_path) do |file|
build.job_artifacts.build(project: build.project, file_type: :junit, file_format: :gzip, file: file)
end
else
artifacts_cache_file(test_reports_failed_path) do |file|
build.job_artifacts.build(project: build.project, file_type: :junit, file_format: :gzip, file: file)
end
end end
end end
...@@ -182,13 +193,21 @@ class Gitlab::Seeder::Pipelines ...@@ -182,13 +193,21 @@ class Gitlab::Seeder::Pipelines
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end end
def artifacts_cache_file(file_path) def test_reports_pass_path
cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_") Rails.root + 'spec/fixtures/junit/junit_ant.xml.gz'
end
FileUtils.copy(file_path, cache_path) def test_reports_failed_path
File.open(cache_path) do |file| Rails.root + 'spec/fixtures/junit/junit.xml.gz'
yield file
end end
def artifacts_cache_file(file_path)
file = Tempfile.new("artifacts")
file.close
FileUtils.copy(file_path, file.path)
yield(UploadedFile.new(file.path, filename: File.basename(file_path)))
end end
end end
......
...@@ -204,3 +204,39 @@ Response: ...@@ -204,3 +204,39 @@ Response:
"deletions": 244 "deletions": 244
}] }]
``` ```
## Merge Base
Get the common ancestor for 2 refs (commit SHAs, branch names or tags).
```
GET /projects/:id/repository/merge_base
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `refs` | array | yes | The refs to find the common ancestor of, for now only 2 refs are supported |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257dcb821665ab5110318fc58a007bd104ed&refs[]=0031876facac3f2b2702a0e53a26e89939a42209"
```
Example response:
```json
{
"id": "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863",
"short_id": "1a0b36b3",
"title": "Initial commit",
"created_at": "2014-02-27T08:03:18.000Z",
"parent_ids": [],
"message": "Initial commit\n",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"authored_date": "2014-02-27T08:03:18.000Z",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T08:03:18.000Z"
}
```
# Working with Merge Request diffs # Working with diffs
Currently we rely on different sources to present merge request diffs, these include: Currently we rely on different sources to present diffs, these include:
- Rugged gem - Rugged gem
- Gitaly service - Gitaly service
...@@ -11,6 +11,8 @@ We're constantly moving Rugged calls to Gitaly and the progress can be followed ...@@ -11,6 +11,8 @@ We're constantly moving Rugged calls to Gitaly and the progress can be followed
## Architecture overview ## Architecture overview
### Merge request diffs
When refreshing a Merge Request (pushing to a source branch, force-pushing to target branch, or if the target branch now contains any commits from the MR) When refreshing a Merge Request (pushing to a source branch, force-pushing to target branch, or if the target branch now contains any commits from the MR)
we fetch the comparison information using `Gitlab::Git::Compare`, which fetches `base` and `head` data using Gitaly and diff between them through we fetch the comparison information using `Gitlab::Git::Compare`, which fetches `base` and `head` data using Gitaly and diff between them through
`Gitlab::Git::Diff.between` (which uses _Gitaly_ if it's enabled, otherwise _Rugged_). `Gitlab::Git::Diff.between` (which uses _Gitaly_ if it's enabled, otherwise _Rugged_).
...@@ -32,6 +34,17 @@ In order to present diffs information on the Merge Request diffs page, we: ...@@ -32,6 +34,17 @@ In order to present diffs information on the Merge Request diffs page, we:
3. If the diff file is cacheable (text-based), it's cached on Redis 3. If the diff file is cacheable (text-based), it's cached on Redis
using `Gitlab::Diff::FileCollection::MergeRequestDiff` using `Gitlab::Diff::FileCollection::MergeRequestDiff`
### Note diffs
When commenting on a diff (any comparison), we persist a truncated diff version
on `NoteDiffFile` (which is associated with the actual `DiffNote`). So instead
of hitting the repository every time we need the diff of the file, we:
1. Check whether we have the `NoteDiffFile#diff` persisted and use it
2. Otherwise, if it's a current MR revision, use the persisted
`MergeRequestDiffFile#diff`
3. In the last scenario, go the the repository and fetch the diff
## Diff limits ## Diff limits
As explained above, we limit single diff files and the size of the whole diff. There are scenarios where we collapse the diff file, As explained above, we limit single diff files and the size of the whole diff. There are scenarios where we collapse the diff file,
......
...@@ -133,3 +133,27 @@ afterEach(() => { ...@@ -133,3 +133,27 @@ afterEach(() => {
vm.$destroy(); vm.$destroy();
}); });
``` ```
## Testing with older browsers
Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps:
### Browserstack
[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers.
You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access.
You can find the credentials on 1Password, under `frontendteam@gitlab.com`.
### Firefox
#### macOS
You can download any older version of Firefox from the releases FTP server, https://ftp.mozilla.org/pub/firefox/releases/
1. From the website, select a version, in this case `50.0.1`.
2. Go to the mac folder.
3. Select your preferred language, you will find the dmg package inside, download it.
4. Drag and drop the application to any other folder but the `Applications` folder.
5. Rename the application to something like `Firefox_Old`.
6. Move the application to the `Applications` folder.
7. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version.
8. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version.
...@@ -235,6 +235,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji ...@@ -235,6 +235,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
:zap: You can use emoji anywhere GFM is supported. :v: :zap: You can use emoji anywhere GFM is supported. :v:
...@@ -245,6 +247,8 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil ...@@ -245,6 +247,8 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
### Special GitLab References ### Special GitLab References
GFM recognizes special references. GFM recognizes special references.
......
...@@ -72,5 +72,39 @@ leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list ...@@ -72,5 +72,39 @@ leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list
of branches. You will need to commit or discard all your changes before of branches. You will need to commit or discard all your changes before
switching to a different branch. switching to a different branch.
## Client Side Evaluation
> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19764) [GitLab Core][ce] 11.2.
The Web IDE can be used to preview JavaScript projects right in the browser.
This feature uses CodeSandbox to compile and bundle the JavaScript used to
preview the web application. On public projects, an `Open in CodeSandbox`
button is visible which will transfer the contents of the project into a
CodeSandbox project to share with others.
**Note** this button is not visible on private or internal projects.
![Web IDE Client Side Evaluation](img/clientside_evaluation.png)
### Enabling Client Side Evaluation
The Client Side Evaluation feature needs to be enabled in the GitLab instances
admin settings. Client Side Evaluation is enabled for all projects on
GitLab.com
![Admin Client Side Evaluation setting](img/admin_clientside_evaluation.png)
Once it has been enabled in application settings, projects with a
`package.json` file and a `main` entry point can be previewed inside of the Web
IDE. An example `package.json` is below.
```json
{
"main": "index.js",
"dependencies": {
"vue": "latest"
}
}
```
[ce]: https://about.gitlab.com/pricing/ [ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/
...@@ -123,6 +123,39 @@ module API ...@@ -123,6 +123,39 @@ module API
not_found! not_found!
end end
end end
desc 'Get the common ancestor between commits' do
success Entities::Commit
end
params do
# For now we just support 2 refs passed, but `merge-base` supports
# multiple defining this as an Array instead of 2 separate params will
# make sure we don't need to deprecate this API in favor of one
# supporting multiple commits when this functionality gets added to
# Gitaly
requires :refs, type: Array[String]
end
get ':id/repository/merge_base' do
refs = params[:refs]
unless refs.size == 2
render_api_error!('Provide exactly 2 refs', 400)
end
merge_base = Gitlab::Git::MergeBase.new(user_project.repository, refs)
if merge_base.unknown_refs.any?
ref_noun = 'ref'.pluralize(merge_base.unknown_refs.size)
message = "Could not find #{ref_noun}: #{merge_base.unknown_refs.join(', ')}"
render_api_error!(message, 400)
end
if merge_base.commit
present merge_base.commit, with: Entities::Commit
else
not_found!("Merge Base")
end
end
end end
end end
end end
...@@ -10,9 +10,11 @@ module Gitlab ...@@ -10,9 +10,11 @@ module Gitlab
TAG_REF_PREFIX = "refs/tags/".freeze TAG_REF_PREFIX = "refs/tags/".freeze
BRANCH_REF_PREFIX = "refs/heads/".freeze BRANCH_REF_PREFIX = "refs/heads/".freeze
CommandError = Class.new(StandardError) BaseError = Class.new(StandardError)
CommitError = Class.new(StandardError) CommandError = Class.new(BaseError)
OSError = Class.new(StandardError) CommitError = Class.new(BaseError)
OSError = Class.new(BaseError)
UnknownRef = Class.new(BaseError)
class << self class << self
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
......
# frozen_string_literal: true
module Gitlab
module Git
class MergeBase
include Gitlab::Utils::StrongMemoize
def initialize(repository, refs)
@repository, @refs = repository, refs
end
# Returns the SHA of the first common ancestor
def sha
if unknown_refs.any?
raise UnknownRef, "Can't find merge base for unknown refs: #{unknown_refs.inspect}"
end
strong_memoize(:sha) do
@repository.merge_base(*commits_for_refs)
end
end
# Returns the merge base as a Gitlab::Git::Commit
def commit
return unless sha
@commit ||= @repository.commit_by(oid: sha)
end
# Returns the refs passed on initialization that aren't found in
# the repository, and thus cannot be used to find a merge base.
def unknown_refs
@unknown_refs ||= Hash[@refs.zip(commits_for_refs)]
.select { |ref, commit| commit.nil? }.keys
end
private
def commits_for_refs
@commits_for_refs ||= @repository.commits_by(oids: @refs)
end
end
end
end
...@@ -2406,6 +2406,9 @@ msgstr "" ...@@ -2406,6 +2406,9 @@ msgstr ""
msgid "Date picker" msgid "Date picker"
msgstr "" msgstr ""
msgid "Debug"
msgstr ""
msgid "Dec" msgid "Dec"
msgstr "" msgstr ""
...@@ -2963,6 +2966,9 @@ msgstr "" ...@@ -2963,6 +2966,9 @@ msgstr ""
msgid "Error updating todo status." msgid "Error updating todo status."
msgstr "" msgstr ""
msgid "Error while loading the merge request. Please try again."
msgstr ""
msgid "Estimated" msgid "Estimated"
msgstr "" msgstr ""
...@@ -3577,6 +3583,9 @@ msgstr "" ...@@ -3577,6 +3583,9 @@ msgstr ""
msgid "Go back" msgid "Go back"
msgstr "" msgstr ""
msgid "Go to"
msgstr ""
msgid "Go to %{link_to_google_takeout}." msgid "Go to %{link_to_google_takeout}."
msgstr "" msgstr ""
...@@ -4058,6 +4067,9 @@ msgstr "" ...@@ -4058,6 +4067,9 @@ msgstr ""
msgid "Jobs" msgid "Jobs"
msgstr "" msgstr ""
msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it."
msgstr ""
msgid "Jul" msgid "Jul"
msgstr "" msgstr ""
...@@ -6062,6 +6074,9 @@ msgstr "" ...@@ -6062,6 +6074,9 @@ msgstr ""
msgid "Runners can be placed on separate users, servers, and even on your local machine." msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr "" msgstr ""
msgid "Runners page"
msgstr ""
msgid "Runners page." msgid "Runners page."
msgstr "" msgstr ""
...@@ -7049,6 +7064,12 @@ msgstr "" ...@@ -7049,6 +7064,12 @@ msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner" msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr "" msgstr ""
msgid "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:"
msgstr ""
msgid "This job is stuck, because you don't have any active runners that can run this job."
msgstr ""
msgid "This job requires a manual action" msgid "This job requires a manual action"
msgstr "" msgstr ""
......
...@@ -162,6 +162,10 @@ describe ApplicationController do ...@@ -162,6 +162,10 @@ describe ApplicationController do
describe 'session expiration' do describe 'session expiration' do
controller(described_class) do controller(described_class) do
# The anonymous controller will report 401 and fail to run any actions.
# Normally, GitLab will just redirect you to sign in.
skip_before_action :authenticate_user!, only: :index
def index def index
render text: 'authenticated' render text: 'authenticated'
end end
......
...@@ -18,6 +18,7 @@ describe 'Blob shortcuts', :js do ...@@ -18,6 +18,7 @@ describe 'Blob shortcuts', :js do
describe 'pressing "y"' do describe 'pressing "y"' do
it 'redirects to permalink with commit sha' do it 'redirects to permalink with commit sha' do
visit_blob visit_blob
wait_for_requests
find('body').native.send_key('y') find('body').native.send_key('y')
...@@ -27,6 +28,7 @@ describe 'Blob shortcuts', :js do ...@@ -27,6 +28,7 @@ describe 'Blob shortcuts', :js do
it 'maintains fragment hash when redirecting' do it 'maintains fragment hash when redirecting' do
fragment = "L1" fragment = "L1"
visit_blob(fragment) visit_blob(fragment)
wait_for_requests
find('body').native.send_key('y') find('body').native.send_key('y')
......
...@@ -213,6 +213,7 @@ describe "User browses files" do ...@@ -213,6 +213,7 @@ describe "User browses files" do
context "when browsing a file content", :js do context "when browsing a file content", :js do
before do before do
visit(tree_path_root_ref) visit(tree_path_root_ref)
wait_for_requests
click_link(".gitignore") click_link(".gitignore")
end end
......
...@@ -19,6 +19,7 @@ describe 'Projects > Files > User deletes files', :js do ...@@ -19,6 +19,7 @@ describe 'Projects > Files > User deletes files', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
visit(project_tree_path_root_ref) visit(project_tree_path_root_ref)
wait_for_requests
end end
it 'deletes the file', :js do it 'deletes the file', :js do
...@@ -35,10 +36,11 @@ describe 'Projects > Files > User deletes files', :js do ...@@ -35,10 +36,11 @@ describe 'Projects > Files > User deletes files', :js do
end end
end end
context 'when an user does not have write access' do context 'when an user does not have write access', :js do
before do before do
project2.add_reporter(user) project2.add_reporter(user)
visit(project2_tree_path_root_ref) visit(project2_tree_path_root_ref)
wait_for_requests
end end
it 'deletes the file in a forked project', :js do it 'deletes the file in a forked project', :js do
......
...@@ -29,13 +29,14 @@ describe 'Projects > Files > User edits files', :js do ...@@ -29,13 +29,14 @@ describe 'Projects > Files > User edits files', :js do
end end
end end
context 'when an user has write access' do context 'when an user has write access', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
visit(project_tree_path_root_ref) visit(project_tree_path_root_ref)
wait_for_requests
end end
it 'inserts a content of a file', :js do it 'inserts a content of a file' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
find('.file-editor', match: :first) find('.file-editor', match: :first)
...@@ -49,13 +50,14 @@ describe 'Projects > Files > User edits files', :js do ...@@ -49,13 +50,14 @@ describe 'Projects > Files > User edits files', :js do
it 'does not show the edit link if a file is binary' do it 'does not show the edit link if a file is binary' do
binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png') binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png')
visit(project_blob_path(project, binary_file)) visit(project_blob_path(project, binary_file))
wait_for_requests
page.within '.content' do page.within '.content' do
expect(page).not_to have_link('edit') expect(page).not_to have_link('edit')
end end
end end
it 'commits an edited file', :js do it 'commits an edited file' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
find('.file-editor', match: :first) find('.file-editor', match: :first)
...@@ -72,7 +74,7 @@ describe 'Projects > Files > User edits files', :js do ...@@ -72,7 +74,7 @@ describe 'Projects > Files > User edits files', :js do
expect(page).to have_content('*.rbca') expect(page).to have_content('*.rbca')
end end
it 'commits an edited file to a new branch', :js do it 'commits an edited file to a new branch' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
...@@ -91,7 +93,7 @@ describe 'Projects > Files > User edits files', :js do ...@@ -91,7 +93,7 @@ describe 'Projects > Files > User edits files', :js do
expect(page).to have_content('*.rbca') expect(page).to have_content('*.rbca')
end end
it 'shows the diff of an edited file', :js do it 'shows the diff of an edited file' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
find('.file-editor', match: :first) find('.file-editor', match: :first)
...@@ -106,13 +108,14 @@ describe 'Projects > Files > User edits files', :js do ...@@ -106,13 +108,14 @@ describe 'Projects > Files > User edits files', :js do
it_behaves_like 'unavailable for an archived project' it_behaves_like 'unavailable for an archived project'
end end
context 'when an user does not have write access' do context 'when an user does not have write access', :js do
before do before do
project2.add_reporter(user) project2.add_reporter(user)
visit(project2_tree_path_root_ref) visit(project2_tree_path_root_ref)
wait_for_requests
end end
it 'inserts a content of a file in a forked project', :js do it 'inserts a content of a file in a forked project' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
...@@ -134,7 +137,7 @@ describe 'Projects > Files > User edits files', :js do ...@@ -134,7 +137,7 @@ describe 'Projects > Files > User edits files', :js do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end end
it 'commits an edited file in a forked project', :js do it 'commits an edited file in a forked project' do
click_link('.gitignore') click_link('.gitignore')
find('.js-edit-blob').click find('.js-edit-blob').click
...@@ -163,6 +166,7 @@ describe 'Projects > Files > User edits files', :js do ...@@ -163,6 +166,7 @@ describe 'Projects > Files > User edits files', :js do
let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) }
before do before do
visit(project2_tree_path_root_ref) visit(project2_tree_path_root_ref)
wait_for_requests
end end
it 'links to the forked project for editing' do it 'links to the forked project for editing' do
......
...@@ -21,9 +21,10 @@ describe 'Projects > Files > User replaces files', :js do ...@@ -21,9 +21,10 @@ describe 'Projects > Files > User replaces files', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
visit(project_tree_path_root_ref) visit(project_tree_path_root_ref)
wait_for_requests
end end
it 'replaces an existed file with a new one', :js do it 'replaces an existed file with a new one' do
click_link('.gitignore') click_link('.gitignore')
expect(page).to have_content('.gitignore') expect(page).to have_content('.gitignore')
...@@ -47,9 +48,10 @@ describe 'Projects > Files > User replaces files', :js do ...@@ -47,9 +48,10 @@ describe 'Projects > Files > User replaces files', :js do
before do before do
project2.add_reporter(user) project2.add_reporter(user)
visit(project2_tree_path_root_ref) visit(project2_tree_path_root_ref)
wait_for_requests
end end
it 'replaces an existed file with a new one in a forked project', :js do it 'replaces an existed file with a new one in a forked project' do
click_link('.gitignore') click_link('.gitignore')
expect(page).to have_content('.gitignore') expect(page).to have_content('.gitignore')
......
...@@ -46,4 +46,20 @@ describe('IDE new entry dropdown button component', () => { ...@@ -46,4 +46,20 @@ describe('IDE new entry dropdown button component', () => {
done(); done();
}); });
}); });
describe('tooltipTitle', () => {
it('returns empty string when showLabel is true', () => {
expect(vm.tooltipTitle).toBe('');
});
it('returns label', done => {
vm.showLabel = false;
vm.$nextTick(() => {
expect(vm.tooltipTitle).toBe('Testing');
done();
});
});
});
}); });
...@@ -292,6 +292,8 @@ describe('IDE clientside preview', () => { ...@@ -292,6 +292,8 @@ describe('IDE clientside preview', () => {
describe('update', () => { describe('update', () => {
beforeEach(() => { beforeEach(() => {
jasmine.clock().install(); jasmine.clock().install();
vm.sandpackReady = true;
vm.manager.updatePreview = jasmine.createSpy('updatePreview'); vm.manager.updatePreview = jasmine.createSpy('updatePreview');
vm.manager.listener = jasmine.createSpy('updatePreview'); vm.manager.listener = jasmine.createSpy('updatePreview');
}); });
...@@ -306,7 +308,7 @@ describe('IDE clientside preview', () => { ...@@ -306,7 +308,7 @@ describe('IDE clientside preview', () => {
vm.update(); vm.update();
jasmine.clock().tick(500); jasmine.clock().tick(250);
expect(vm.initPreview).toHaveBeenCalled(); expect(vm.initPreview).toHaveBeenCalled();
}); });
...@@ -314,7 +316,7 @@ describe('IDE clientside preview', () => { ...@@ -314,7 +316,7 @@ describe('IDE clientside preview', () => {
it('calls updatePreview', () => { it('calls updatePreview', () => {
vm.update(); vm.update();
jasmine.clock().tick(500); jasmine.clock().tick(250);
expect(vm.manager.updatePreview).toHaveBeenCalledWith(vm.sandboxOpts); expect(vm.manager.updatePreview).toHaveBeenCalledWith(vm.sandboxOpts);
}); });
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores'; import store from '~/ide/stores';
import { import actions, {
getMergeRequestData, getMergeRequestData,
getMergeRequestChanges, getMergeRequestChanges,
getMergeRequestVersions, getMergeRequestVersions,
openMergeRequest,
} from '~/ide/stores/actions/merge_request'; } from '~/ide/stores/actions/merge_request';
import service from '~/ide/services'; import service from '~/ide/services';
import { activityBarViews } from '~/ide/constants';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
describe('IDE store merge request actions', () => { describe('IDE store merge request actions', () => {
...@@ -238,4 +240,101 @@ describe('IDE store merge request actions', () => { ...@@ -238,4 +240,101 @@ describe('IDE store merge request actions', () => {
}); });
}); });
}); });
describe('openMergeRequest', () => {
const mr = {
projectId: 'abcproject',
targetProjectId: 'defproject',
mergeRequestId: 2,
};
let testMergeRequest;
let testMergeRequestChanges;
beforeEach(() => {
testMergeRequest = {
source_branch: 'abcbranch',
};
testMergeRequestChanges = {
changes: [],
};
store.state.entries = {
foo: {},
bar: {},
};
spyOn(store, 'dispatch').and.callFake((type) => {
switch (type) {
case 'getMergeRequestData':
return Promise.resolve(testMergeRequest);
case 'getMergeRequestChanges':
return Promise.resolve(testMergeRequestChanges);
default:
return Promise.resolve();
}
});
});
it('dispatch actions for merge request data', done => {
openMergeRequest(store, mr)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['getMergeRequestData', mr],
['setCurrentBranchId', testMergeRequest.source_branch],
['getBranchData', {
projectId: mr.projectId,
branchId: testMergeRequest.source_branch,
}],
['getFiles', {
projectId: mr.projectId,
branchId: testMergeRequest.source_branch,
}],
['getMergeRequestVersions', mr],
['getMergeRequestChanges', mr],
]);
})
.then(done)
.catch(done.fail);
});
it('updates activity bar view and gets file data, if changes are found', done => {
testMergeRequestChanges.changes = [
{ new_path: 'foo' },
{ new_path: 'bar' },
];
openMergeRequest(store, mr)
.then(() => {
expect(store.dispatch).toHaveBeenCalledWith('updateActivityBarView', activityBarViews.review);
testMergeRequestChanges.changes.forEach((change, i) => {
expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', {
file: store.state.entries[change.new_path],
mrChange: change,
});
expect(store.dispatch).toHaveBeenCalledWith('getFileData', {
path: change.new_path,
makeFileActive: i === 0,
});
});
})
.then(done)
.catch(done.fail);
});
it('flashes message, if error', done => {
const flashSpy = spyOnDependency(actions, 'flash');
store.dispatch.and.returnValue(Promise.reject());
openMergeRequest(store, mr)
.then(() => {
fail('Expected openMergeRequest to throw an error');
})
.catch(() => {
expect(flashSpy).toHaveBeenCalledWith(jasmine.any(String));
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
showBranchNotFoundError, showBranchNotFoundError,
createNewBranchFromDefault, createNewBranchFromDefault,
getBranchData, getBranchData,
openBranch,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
import service from '~/ide/services'; import service from '~/ide/services';
...@@ -224,4 +225,55 @@ describe('IDE store project actions', () => { ...@@ -224,4 +225,55 @@ describe('IDE store project actions', () => {
}); });
}); });
}); });
describe('openBranch', () => {
const branch = {
projectId: 'feature/lorem-ipsum',
branchId: '123-lorem',
};
beforeEach(() => {
store.state.entries = {
foo: { pending: false },
'foo/bar-pending': { pending: true },
'foo/bar': { pending: false },
};
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
it('dispatches branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['getFiles', branch],
]);
})
.then(done)
.catch(done.fail);
});
it('handles tree entry action, if basePath is given', done => {
openBranch(store, { ...branch, basePath: 'foo/bar/' })
.then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'handleTreeEntryAction',
store.state.entries['foo/bar'],
);
})
.then(done)
.catch(done.fail);
});
it('does not handle tree entry action, if entry is pending', done => {
openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything());
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -9,7 +9,6 @@ import { ...@@ -9,7 +9,6 @@ import {
receiveBranchesSuccess, receiveBranchesSuccess,
fetchBranches, fetchBranches,
resetBranches, resetBranches,
openBranch,
} from '~/ide/stores/modules/branches/actions'; } from '~/ide/stores/modules/branches/actions';
import { branches, projectData } from '../../../mock_data'; import { branches, projectData } from '../../../mock_data';
...@@ -174,20 +173,5 @@ describe('IDE branches actions', () => { ...@@ -174,20 +173,5 @@ describe('IDE branches actions', () => {
); );
}); });
}); });
describe('openBranch', () => {
it('dispatches goToRoute action with path', done => {
const branchId = branches[0].name;
const expectedPath = `/project/${projectData.name_with_namespace}/edit/${branchId}`;
testAction(
openBranch,
branchId,
mockedState,
[],
[{ type: 'goToRoute', payload: expectedPath }],
done,
);
});
});
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue';
import job from './mock_data'; import job from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Sidebar details block', () => { describe('Sidebar details block', () => {
let SidebarComponent; let SidebarComponent;
...@@ -20,39 +21,53 @@ describe('Sidebar details block', () => { ...@@ -20,39 +21,53 @@ describe('Sidebar details block', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render a loading spinner', () => { it('should render a loading spinner', () => {
vm = new SidebarComponent({ vm = mountComponent(SidebarComponent, {
propsData: {
job: {}, job: {},
isLoading: true, isLoading: true,
}, });
}).$mount();
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
}); });
}); });
describe("when user can't retry", () => { describe('when there is no retry path retry', () => {
it('should not render a retry button', () => { it('should not render a retry button', () => {
vm = new SidebarComponent({ vm = mountComponent(SidebarComponent, {
propsData: {
job: {}, job: {},
canUserRetry: false, isLoading: false,
isLoading: true, });
},
}).$mount();
expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
}); });
}); });
describe('without terminal path', () => {
it('does not render terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
});
expect(vm.$el.querySelector('.js-terminal-link')).toBeNull();
});
});
describe('with terminal path', () => {
it('renders terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
terminalPath: 'job/43123/terminal',
});
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
});
});
beforeEach(() => { beforeEach(() => {
vm = new SidebarComponent({ vm = mountComponent(SidebarComponent, {
propsData: {
job, job,
canUserRetry: true,
isLoading: false, isLoading: false,
}, });
}).$mount();
}); });
describe('actions', () => { describe('actions', () => {
...@@ -102,13 +117,15 @@ describe('Sidebar details block', () => { ...@@ -102,13 +117,15 @@ describe('Sidebar details block', () => {
}); });
it('should render runner ID', () => { it('should render runner ID', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)'); expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual(
'Runner: local ci runner (#1)',
);
}); });
it('should render timeout information', () => { it('should render timeout information', () => {
expect( expect(trimWhitespace(vm.$el.querySelector('.js-job-timeout'))).toEqual(
trimWhitespace(vm.$el.querySelector('.js-job-timeout')), 'Timeout: 1m 40s (from runner)',
).toEqual('Timeout: 1m 40s (from runner)'); );
}); });
it('should render coverage', () => { it('should render coverage', () => {
......
import Vue from 'vue';
import component from '~/jobs/components/stuck_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Stuck Block Job component', () => {
const Component = Vue.extend(component);
let vm;
afterEach(() => {
vm.$destroy();
});
describe('with no runners for project', () => {
beforeEach(() => {
vm = mountComponent(Component, {
hasNoRunnersForProject: true,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
it('renders only information about project not having runners', () => {
expect(vm.$el.querySelector('.js-stuck-no-runners')).not.toBeNull();
expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull();
expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull();
});
it('renders link to runners page', () => {
expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual(
'/root/project/runners#js-runners-settings',
);
});
});
describe('with tags', () => {
beforeEach(() => {
vm = mountComponent(Component, {
hasNoRunnersForProject: false,
tags: ['docker', 'gitlab-org'],
runnersPath: '/root/project/runners#js-runners-settings',
});
});
it('renders information about the tags not being set', () => {
expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull();
expect(vm.$el.querySelector('.js-stuck-with-tags')).not.toBeNull();
expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull();
});
it('renders tags', () => {
expect(vm.$el.textContent).toContain('docker');
expect(vm.$el.textContent).toContain('gitlab-org');
});
it('renders link to runners page', () => {
expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual(
'/root/project/runners#js-runners-settings',
);
});
});
describe('without active runners', () => {
beforeEach(() => {
vm = mountComponent(Component, {
hasNoRunnersForProject: false,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
it('renders information about project not having runners', () => {
expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull();
expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull();
expect(vm.$el.querySelector('.js-stuck-no-active-runner')).not.toBeNull();
});
it('renders link to runners page', () => {
expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual(
'/root/project/runners#js-runners-settings',
);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Git::MergeBase do
set(:project) { create(:project, :repository) }
let(:repository) { project.repository }
subject(:merge_base) { described_class.new(repository, refs) }
shared_context 'existing refs with a merge base', :existing_refs do
let(:refs) do
%w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209)
end
end
shared_context 'when passing a missing ref', :missing_ref do
let(:refs) do
%w(304d257dcb821665ab5110318fc58a007bd104ed aaaa)
end
end
shared_context 'when passing refs that do not have a common ancestor', :no_common_ancestor do
let(:refs) { ['304d257dcb821665ab5110318fc58a007bd104ed', TestEnv::BRANCH_SHA['orphaned-branch']] }
end
describe '#sha' do
context 'when the refs exist', :existing_refs do
it 'returns the SHA of the merge base' do
expect(merge_base.sha).not_to be_nil
end
it 'memoizes the result' do
expect(repository).to receive(:merge_base).once.and_call_original
2.times { merge_base.sha }
end
end
context 'when passing a missing ref', :missing_ref do
it 'does not call merge_base on the repository but raises an error' do
expect(repository).not_to receive(:merge_base)
expect { merge_base.sha }.to raise_error(Gitlab::Git::UnknownRef)
end
end
it 'returns `nil` when the refs do not have a common ancestor', :no_common_ancestor do
expect(merge_base.sha).to be_nil
end
it 'returns a merge base when passing 2 branch names' do
merge_base = described_class.new(repository, %w(master feature))
expect(merge_base.sha).to be_present
end
it 'returns a merge base when passing a tag name' do
merge_base = described_class.new(repository, %w(master v1.0.0))
expect(merge_base.sha).to be_present
end
end
describe '#commit' do
context 'for existing refs with a merge base', :existing_refs do
it 'finds the commit for the merge base' do
expect(merge_base.commit).to be_a(Commit)
end
it 'only looks up the commit once' do
expect(repository).to receive(:commit_by).once.and_call_original
2.times { merge_base.commit }
end
end
it 'does not try to find the commit when there is no sha', :no_common_ancestor do
expect(repository).not_to receive(:commit_by)
merge_base.commit
end
end
describe '#unknown_refs', :missing_ref do
it 'returns the the refs passed that are not part of the repository' do
expect(merge_base.unknown_refs).to contain_exactly('aaaa')
end
it 'only looks up the commits once' do
expect(merge_base).to receive(:commits_for_refs).once.and_call_original
2.times { merge_base.unknown_refs }
end
end
end
...@@ -465,4 +465,77 @@ describe API::Repositories do ...@@ -465,4 +465,77 @@ describe API::Repositories do
end end
end end
end end
describe 'GET :id/repository/merge_base' do
let(:refs) do
%w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209)
end
subject(:request) do
get(api("/projects/#{project.id}/repository/merge_base", current_user), refs: refs)
end
shared_examples 'merge base' do
it 'returns the common ancestor' do
request
expect(response).to have_gitlab_http_status(:success)
expect(json_response['id']).to be_present
end
end
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'merge base' do
let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
let(:current_user) { nil }
let(:message) { '404 Project Not Found' }
end
end
context 'when authenticated', 'as a developer' do
it_behaves_like 'merge base' do
let(:current_user) { user }
end
end
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:current_user) { guest }
end
end
context 'when passing refs that do not exist' do
it_behaves_like '400 response' do
let(:refs) { %w(304d257dcb821665ab5110318fc58a007bd104ed missing) }
let(:current_user) { user }
let(:message) { 'Could not find ref: missing' }
end
end
context 'when passing refs that do not have a merge base' do
it_behaves_like '404 response' do
let(:refs) { ['304d257dcb821665ab5110318fc58a007bd104ed', TestEnv::BRANCH_SHA['orphaned-branch']] }
let(:current_user) { user }
let(:message) { '404 Merge Base Not Found' }
end
end
context 'when not enough refs are passed' do
let(:refs) { %w(only-one) }
let(:current_user) { user }
it 'renders a bad request error' do
request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Provide exactly 2 refs')
end
end
end
end end
...@@ -720,10 +720,29 @@ rollout 100%: ...@@ -720,10 +720,29 @@ rollout 100%:
if [[ -f Dockerfile ]]; then if [[ -f Dockerfile ]]; then
echo "Building Dockerfile-based application..." echo "Building Dockerfile-based application..."
docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . docker build \
--build-arg HTTP_PROXY="$HTTP_PROXY" \
--build-arg http_proxy="$http_proxy" \
--build-arg HTTPS_PROXY="$HTTPS_PROXY" \
--build-arg https_proxy="$https_proxy" \
--build-arg FTP_PROXY="$FTP_PROXY" \
--build-arg ftp_proxy="$ftp_proxy" \
--build-arg NO_PROXY="$NO_PROXY" \
--build-arg no_proxy="$no_proxy" \
-t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .
else else
echo "Building Heroku-based application using gliderlabs/herokuish docker image..." echo "Building Heroku-based application using gliderlabs/herokuish docker image..."
docker run -i -e BUILDPACK_URL --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build docker run -i \
-e BUILDPACK_URL \
-e HTTP_PROXY \
-e http_proxy \
-e HTTPS_PROXY \
-e https_proxy \
-e FTP_PROXY \
-e ftp_proxy \
-e NO_PROXY \
-e no_proxy \
--name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build
docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
docker rm "$CI_CONTAINER_NAME" >/dev/null docker rm "$CI_CONTAINER_NAME" >/dev/null
echo "" echo ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment