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
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.2)
rugged (0.27.4)
safe_yaml (1.0.4)
sanitize (4.6.6)
crass (~> 1.0.2)
......
......@@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) {
canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000';
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
let writeIndex = 0;
testMapKeys.forEach(testKey => {
......
......@@ -61,7 +61,7 @@ export default {
<slot name="header"></slot>
</header>
<div
class="ide-tree-body"
class="ide-tree-body h-100"
>
<repo-file
v-for="file in currentTree.tree"
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
components: {
Icon,
},
......@@ -26,6 +30,11 @@ export default {
default: true,
},
},
computed: {
tooltipTitle() {
return this.showLabel ? '' : this.label;
},
},
methods: {
clicked() {
this.$emit('click');
......@@ -36,7 +45,9 @@ export default {
<template>
<button
v-tooltip
:aria-label="label"
:title="tooltipTitle"
type="button"
class="btn-blank"
@click.stop.prevent="clicked"
......
......@@ -2,6 +2,7 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants';
......@@ -16,6 +17,7 @@ export default {
return {
manager: {},
loading: false,
sandpackReady: false,
};
},
computed: {
......@@ -81,6 +83,10 @@ export default {
}
this.manager = {};
if (this.listener) {
this.listener();
}
clearTimeout(this.timeout);
this.timeout = null;
},
......@@ -96,17 +102,29 @@ export default {
return this.loadFileContent(this.mainEntry)
.then(() => this.$nextTick())
.then(() =>
.then(() => {
this.initManager('#ide-preview', this.sandboxOpts, {
fileResolver: {
isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
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() {
if (this.timeout) return;
if (!this.sandpackReady) return;
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
if (_.isEmpty(this.manager)) {
......@@ -116,10 +134,7 @@ export default {
}
this.manager.updatePreview(this.sandboxOpts);
clearTimeout(this.timeout);
this.timeout = null;
}, 500);
}, 250);
},
initManager(el, opts, resolver) {
this.manager = new Manager(el, opts, resolver);
......
......@@ -3,7 +3,6 @@ import VueRouter from 'vue-router';
import { join as joinPath } from 'path';
import flash from '~/flash';
import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter);
......@@ -74,102 +73,23 @@ router.beforeEach((to, from, next) => {
projectId: to.params.project,
})
.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 mergeRequestId = to.params.mrid;
if (branchId) {
const basePath = to.params[0] || '';
store.dispatch('setCurrentBranchId', branchId);
store.dispatch('getBranchData', {
projectId: fullProjectId,
store.dispatch('openBranch', {
projectId,
branchId,
basePath,
});
} else if (mergeRequestId) {
store.dispatch('openMergeRequest', {
projectId,
mergeRequestId,
targetProjectId: to.query.target_project,
});
store
.dispatch('getFiles', {
projectId: fullProjectId,
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,
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;
});
}
})
.catch(e => {
......
import { __ } from '../../../locale';
import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
import { activityBarViews } from '../../constants';
export const getMergeRequestData = (
{ commit, dispatch, state },
......@@ -104,3 +106,67 @@ export const getMergeRequestVersions = (
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) => {
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 = '' }) => {
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 () => {};
<script>
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
export default {
name: 'SidebarDetailsBlock',
components: {
detailRow,
loadingIcon,
DetailRow,
LoadingIcon,
Icon,
},
mixins: [timeagoMixin],
props: {
......@@ -20,16 +22,16 @@ export default {
type: Boolean,
required: true,
},
canUserRetry: {
type: Boolean,
required: false,
default: false,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
terminalPath: {
type: String,
required: false,
default: null,
},
},
computed: {
shouldRenderContent() {
......@@ -92,7 +94,7 @@ export default {
{{ job.name }}
</strong>
<a
v-if="canUserRetry"
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
......@@ -100,6 +102,16 @@ export default {
>
{{ __('Retry') }}
</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
:aria-label="__('Toggle Sidebar')"
type="button"
......@@ -125,7 +137,7 @@ export default {
{{ __('New issue') }}
</a>
<a
v-if="canUserRetry"
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
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 () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
});
},
......
......@@ -2,7 +2,7 @@ gl-emoji {
font-style: normal;
display: inline-flex;
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;
line-height: 0.9;
}
......@@ -385,7 +385,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
$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
......
......@@ -755,8 +755,19 @@
}
.repository-languages-bar {
height: 6px;
margin-bottom: 8px;
height: 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 {
......
......@@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
before_action :limit_unauthenticated_session_times
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms?
......@@ -27,6 +26,7 @@ class ApplicationController < ActionController::Base
around_action :set_locale
after_action :set_page_title_header, if: :json_request?
after_action :limit_unauthenticated_session_times
protect_from_forgery with: :exception, prepend: true
......
......@@ -103,6 +103,10 @@ class User < ActiveRecord::Base
has_many :groups, through: :group_members
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 :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
# Projects
......@@ -1004,15 +1008,7 @@ class User < ActiveRecord::Base
end
def manageable_groups
union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql
# 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
Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants
end
def namespaces
......@@ -1266,11 +1262,6 @@ class User < ActiveRecord::Base
!terms_accepted?
end
def owned_or_maintainers_groups
union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups])
Group.from("(#{union.to_sql}) namespaces")
end
# @deprecated
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" } }
.sidebar-container
.blocks-container
- if can?(current_user, :create_build_terminal, @build)
.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? } }
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.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
when: 'manual', status: :skipped },
# notify stage
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
{ name: 'slack', stage: 'notify', when: 'manual', status: :success },
]
EXTERNAL_JOBS = [
{ name: 'jenkins', stage: 'test', status: :success,
......@@ -54,16 +54,10 @@ class Gitlab::Seeder::Pipelines
def seed!
pipelines.each do |pipeline|
begin
BUILDS.each { |opts| build_create!(pipeline, opts) }
EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) }
print '.'
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
pipeline.update_duration
pipeline.update_status
end
BUILDS.each { |opts| build_create!(pipeline, opts) }
EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) }
pipeline.update_duration
pipeline.update_status
end
end
......@@ -95,7 +89,9 @@ class Gitlab::Seeder::Pipelines
branch = merge_request.source_branch
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
......@@ -105,7 +101,7 @@ class Gitlab::Seeder::Pipelines
end
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
def build_create!(pipeline, opts = {})
......@@ -118,24 +114,39 @@ class Gitlab::Seeder::Pipelines
# block directly to `Ci::Build#create!`.
setup_artifacts(build)
setup_test_reports(build)
setup_build_log(build)
build.project.environments.
find_or_create_by(name: build.expanded_environment_name)
build.save
build.save!
end
end
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|
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
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
......@@ -182,13 +193,21 @@ class Gitlab::Seeder::Pipelines
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end
def test_reports_pass_path
Rails.root + 'spec/fixtures/junit/junit_ant.xml.gz'
end
def test_reports_failed_path
Rails.root + 'spec/fixtures/junit/junit.xml.gz'
end
def artifacts_cache_file(file_path)
cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
file = Tempfile.new("artifacts")
file.close
FileUtils.copy(file_path, cache_path)
File.open(cache_path) do |file|
yield file
end
FileUtils.copy(file_path, file.path)
yield(UploadedFile.new(file.path, filename: File.basename(file_path)))
end
end
......
......@@ -204,3 +204,39 @@ Response:
"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
- Gitaly service
......@@ -11,6 +11,8 @@ We're constantly moving Rugged calls to Gitaly and the progress can be followed
## 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)
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_).
......@@ -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
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
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(() => {
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
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:
: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
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
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
of branches. You will need to commit or discard all your changes before
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/
[ee]: https://about.gitlab.com/pricing/
......@@ -123,6 +123,39 @@ module API
not_found!
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
......@@ -10,9 +10,11 @@ module Gitlab
TAG_REF_PREFIX = "refs/tags/".freeze
BRANCH_REF_PREFIX = "refs/heads/".freeze
CommandError = Class.new(StandardError)
CommitError = Class.new(StandardError)
OSError = Class.new(StandardError)
BaseError = Class.new(StandardError)
CommandError = Class.new(BaseError)
CommitError = Class.new(BaseError)
OSError = Class.new(BaseError)
UnknownRef = Class.new(BaseError)
class << self
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 ""
msgid "Date picker"
msgstr ""
msgid "Debug"
msgstr ""
msgid "Dec"
msgstr ""
......@@ -2963,6 +2966,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
msgid "Error while loading the merge request. Please try again."
msgstr ""
msgid "Estimated"
msgstr ""
......@@ -3577,6 +3583,9 @@ msgstr ""
msgid "Go back"
msgstr ""
msgid "Go to"
msgstr ""
msgid "Go to %{link_to_google_takeout}."
msgstr ""
......@@ -4058,6 +4067,9 @@ msgstr ""
msgid "Jobs"
msgstr ""
msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it."
msgstr ""
msgid "Jul"
msgstr ""
......@@ -6062,6 +6074,9 @@ msgstr ""
msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
msgid "Runners page"
msgstr ""
msgid "Runners page."
msgstr ""
......@@ -7049,6 +7064,12 @@ msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner"
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"
msgstr ""
......
......@@ -162,6 +162,10 @@ describe ApplicationController do
describe 'session expiration' 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
render text: 'authenticated'
end
......
......@@ -18,6 +18,7 @@ describe 'Blob shortcuts', :js do
describe 'pressing "y"' do
it 'redirects to permalink with commit sha' do
visit_blob
wait_for_requests
find('body').native.send_key('y')
......@@ -27,6 +28,7 @@ describe 'Blob shortcuts', :js do
it 'maintains fragment hash when redirecting' do
fragment = "L1"
visit_blob(fragment)
wait_for_requests
find('body').native.send_key('y')
......
......@@ -213,6 +213,7 @@ describe "User browses files" do
context "when browsing a file content", :js do
before do
visit(tree_path_root_ref)
wait_for_requests
click_link(".gitignore")
end
......
......@@ -19,6 +19,7 @@ describe 'Projects > Files > User deletes files', :js do
before do
project.add_maintainer(user)
visit(project_tree_path_root_ref)
wait_for_requests
end
it 'deletes the file', :js do
......@@ -35,10 +36,11 @@ describe 'Projects > Files > User deletes files', :js do
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
project2.add_reporter(user)
visit(project2_tree_path_root_ref)
wait_for_requests
end
it 'deletes the file in a forked project', :js do
......
......@@ -29,13 +29,14 @@ describe 'Projects > Files > User edits files', :js do
end
end
context 'when an user has write access' do
context 'when an user has write access', :js do
before do
project.add_maintainer(user)
visit(project_tree_path_root_ref)
wait_for_requests
end
it 'inserts a content of a file', :js do
it 'inserts a content of a file' do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
......@@ -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
binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png')
visit(project_blob_path(project, binary_file))
wait_for_requests
page.within '.content' do
expect(page).not_to have_link('edit')
end
end
it 'commits an edited file', :js do
it 'commits an edited file' do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
......@@ -72,7 +74,7 @@ describe 'Projects > Files > User edits files', :js do
expect(page).to have_content('*.rbca')
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')
find('.js-edit-blob').click
......@@ -91,7 +93,7 @@ describe 'Projects > Files > User edits files', :js do
expect(page).to have_content('*.rbca')
end
it 'shows the diff of an edited file', :js do
it 'shows the diff of an edited file' do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
......@@ -106,13 +108,14 @@ describe 'Projects > Files > User edits files', :js do
it_behaves_like 'unavailable for an archived project'
end
context 'when an user does not have write access' do
context 'when an user does not have write access', :js do
before do
project2.add_reporter(user)
visit(project2_tree_path_root_ref)
wait_for_requests
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')
find('.js-edit-blob').click
......@@ -134,7 +137,7 @@ describe 'Projects > Files > User edits files', :js do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
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')
find('.js-edit-blob').click
......@@ -163,6 +166,7 @@ describe 'Projects > Files > User edits files', :js do
let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) }
before do
visit(project2_tree_path_root_ref)
wait_for_requests
end
it 'links to the forked project for editing' do
......
......@@ -21,9 +21,10 @@ describe 'Projects > Files > User replaces files', :js do
before do
project.add_maintainer(user)
visit(project_tree_path_root_ref)
wait_for_requests
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')
expect(page).to have_content('.gitignore')
......@@ -47,9 +48,10 @@ describe 'Projects > Files > User replaces files', :js do
before do
project2.add_reporter(user)
visit(project2_tree_path_root_ref)
wait_for_requests
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')
expect(page).to have_content('.gitignore')
......
......@@ -46,4 +46,20 @@ describe('IDE new entry dropdown button component', () => {
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', () => {
describe('update', () => {
beforeEach(() => {
jasmine.clock().install();
vm.sandpackReady = true;
vm.manager.updatePreview = jasmine.createSpy('updatePreview');
vm.manager.listener = jasmine.createSpy('updatePreview');
});
......@@ -306,7 +308,7 @@ describe('IDE clientside preview', () => {
vm.update();
jasmine.clock().tick(500);
jasmine.clock().tick(250);
expect(vm.initPreview).toHaveBeenCalled();
});
......@@ -314,7 +316,7 @@ describe('IDE clientside preview', () => {
it('calls updatePreview', () => {
vm.update();
jasmine.clock().tick(500);
jasmine.clock().tick(250);
expect(vm.manager.updatePreview).toHaveBeenCalledWith(vm.sandboxOpts);
});
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import {
import actions, {
getMergeRequestData,
getMergeRequestChanges,
getMergeRequestVersions,
openMergeRequest,
} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
import { activityBarViews } from '~/ide/constants';
import { resetStore } from '../../helpers';
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 {
showBranchNotFoundError,
createNewBranchFromDefault,
getBranchData,
openBranch,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
......@@ -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 {
receiveBranchesSuccess,
fetchBranches,
resetBranches,
openBranch,
} from '~/ide/stores/modules/branches/actions';
import { branches, projectData } from '../../../mock_data';
......@@ -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 sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue';
import job from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Sidebar details block', () => {
let SidebarComponent;
......@@ -20,39 +21,53 @@ describe('Sidebar details block', () => {
describe('when it is loading', () => {
it('should render a loading spinner', () => {
vm = new SidebarComponent({
propsData: {
job: {},
isLoading: true,
},
}).$mount();
vm = mountComponent(SidebarComponent, {
job: {},
isLoading: true,
});
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', () => {
vm = new SidebarComponent({
propsData: {
job: {},
canUserRetry: false,
isLoading: true,
},
}).$mount();
vm = mountComponent(SidebarComponent, {
job: {},
isLoading: false,
});
expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
});
});
beforeEach(() => {
vm = new SidebarComponent({
propsData: {
describe('without terminal path', () => {
it('does not render terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
canUserRetry: true,
isLoading: false,
},
}).$mount();
});
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(() => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
});
});
describe('actions', () => {
......@@ -102,13 +117,15 @@ describe('Sidebar details block', () => {
});
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', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-timeout')),
).toEqual('Timeout: 1m 40s (from runner)');
expect(trimWhitespace(vm.$el.querySelector('.js-job-timeout'))).toEqual(
'Timeout: 1m 40s (from runner)',
);
});
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
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
......@@ -720,10 +720,29 @@ rollout 100%:
if [[ -f Dockerfile ]]; then
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
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 rm "$CI_CONTAINER_NAME" >/dev/null
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