Commit 81c8c98e authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-support-bitbucket-server-import

parents 0d9f74d2 fabf6a56
...@@ -359,7 +359,7 @@ GEM ...@@ -359,7 +359,7 @@ GEM
grape-entity (0.7.1) grape-entity (0.7.1)
activesupport (>= 4.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-path-helpers (1.0.5) grape-path-helpers (1.0.6)
activesupport (>= 4, < 5.1) activesupport (>= 4, < 5.1)
grape (~> 1.0) grape (~> 1.0)
rake (~> 12) rake (~> 12)
......
...@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; ...@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue'; import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { rightSidebarViews } from '../constants';
export default { export default {
components: { components: {
...@@ -49,6 +50,7 @@ export default { ...@@ -49,6 +50,7 @@ export default {
this.stopPipelinePolling(); this.stopPipelinePolling();
}, },
methods: { methods: {
...mapActions(['setRightPane']),
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() { startTimer() {
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
...@@ -69,24 +71,31 @@ export default { ...@@ -69,24 +71,31 @@ export default {
return `${this.currentProject.web_url}/commit/${shortSha}`; return `${this.currentProject.web_url}/commit/${shortSha}`;
}, },
}, },
rightSidebarViews,
}; };
</script> </script>
<template> <template>
<footer class="ide-status-bar"> <footer class="ide-status-bar">
<div <div
v-if="lastCommit && lastCommitFormatedAge" v-if="lastCommit"
class="ide-status-branch" class="ide-status-branch"
> >
<span <span
v-if="latestPipeline && latestPipeline.details" v-if="latestPipeline && latestPipeline.details"
class="ide-status-pipeline" class="ide-status-pipeline"
> >
<ci-icon <button
v-tooltip type="button"
:status="latestPipeline.details.status" class="p-0 border-0 h-50"
:title="latestPipeline.details.status.text" @click="setRightPane($options.rightSidebarViews.pipelines)"
/> >
<ci-icon
v-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
</button>
Pipeline Pipeline
<a <a
:href="latestPipeline.details.status.details_path" :href="latestPipeline.details.status.details_path"
......
...@@ -101,6 +101,7 @@ router.beforeEach((to, from, next) => { ...@@ -101,6 +101,7 @@ router.beforeEach((to, from, next) => {
store store
.dispatch('getMergeRequestData', { .dispatch('getMergeRequestData', {
projectId: fullProjectId, projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid, mergeRequestId: to.params.mrid,
}) })
.then(mr => { .then(mr => {
...@@ -119,12 +120,14 @@ router.beforeEach((to, from, next) => { ...@@ -119,12 +120,14 @@ router.beforeEach((to, from, next) => {
.then(() => .then(() =>
store.dispatch('getMergeRequestVersions', { store.dispatch('getMergeRequestVersions', {
projectId: fullProjectId, projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid, mergeRequestId: to.params.mrid,
}), }),
) )
.then(() => .then(() =>
store.dispatch('getMergeRequestChanges', { store.dispatch('getMergeRequestChanges', {
projectId: fullProjectId, projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid, mergeRequestId: to.params.mrid,
}), }),
) )
......
...@@ -4,12 +4,14 @@ import * as types from '../mutation_types'; ...@@ -4,12 +4,14 @@ import * as types from '../mutation_types';
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, dispatch, state }, { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, targetProjectId = null, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) { if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service service
.getProjectMergeRequestData(projectId, mergeRequestId, { render_html: true }) .getProjectMergeRequestData(targetProjectId || projectId, mergeRequestId, {
render_html: true,
})
.then(({ data }) => { .then(({ data }) => {
commit(types.SET_MERGE_REQUEST, { commit(types.SET_MERGE_REQUEST, {
projectPath: projectId, projectPath: projectId,
...@@ -38,12 +40,12 @@ export const getMergeRequestData = ( ...@@ -38,12 +40,12 @@ export const getMergeRequestData = (
export const getMergeRequestChanges = ( export const getMergeRequestChanges = (
{ commit, dispatch, state }, { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, targetProjectId = null, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) { if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service service
.getProjectMergeRequestChanges(projectId, mergeRequestId) .getProjectMergeRequestChanges(targetProjectId || projectId, mergeRequestId)
.then(({ data }) => { .then(({ data }) => {
commit(types.SET_MERGE_REQUEST_CHANGES, { commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId, projectPath: projectId,
...@@ -71,12 +73,12 @@ export const getMergeRequestChanges = ( ...@@ -71,12 +73,12 @@ export const getMergeRequestChanges = (
export const getMergeRequestVersions = ( export const getMergeRequestVersions = (
{ commit, dispatch, state }, { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, targetProjectId = null, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) { if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
service service
.getProjectMergeRequestVersions(projectId, mergeRequestId) .getProjectMergeRequestVersions(targetProjectId || projectId, mergeRequestId)
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
commit(types.SET_MERGE_REQUEST_VERSIONS, { commit(types.SET_MERGE_REQUEST_VERSIONS, {
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue'; import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue'; import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue'; import simpleMetric from './simple_metric.vue';
import Flash from '../../flash';
export default { export default {
components: { components: {
detailedMetric, detailedMetric,
...@@ -69,37 +66,13 @@ export default { ...@@ -69,37 +66,13 @@ export default {
}, },
}, },
mounted() { mounted() {
this.interceptor = PerformanceBarService.registerInterceptor(
this.peekUrl,
this.loadRequestDetails,
);
this.loadRequestDetails(this.requestId, window.location.href);
this.currentRequest = this.requestId; this.currentRequest = this.requestId;
if (this.lineProfileModal.length) { if (this.lineProfileModal.length) {
this.lineProfileModal.modal('toggle'); this.lineProfileModal.modal('toggle');
} }
}, },
beforeDestroy() {
PerformanceBarService.removeInterceptor(this.interceptor);
},
methods: { methods: {
loadRequestDetails(requestId, requestUrl) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
}
this.store.addRequest(requestId, requestUrl);
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data.data);
})
.catch(() =>
Flash(`Error getting performance bar results for ${requestId}`),
);
},
changeCurrentRequest(newRequestId) { changeCurrentRequest(newRequestId) {
this.currentRequest = newRequestId; this.currentRequest = newRequestId;
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue'; import Flash from '../flash';
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store'; import PerformanceBarStore from './stores/performance_bar_store';
export default ({ container }) => export default ({ container }) =>
new Vue({ new Vue({
el: container, el: container,
components: { components: {
performanceBarApp, performanceBarApp: () => import('./components/performance_bar_app.vue'),
}, },
data() { data() {
const performanceBarData = document.querySelector(this.$options.el) const performanceBarData = document.querySelector(this.$options.el)
...@@ -21,6 +22,34 @@ export default ({ container }) => ...@@ -21,6 +22,34 @@ export default ({ container }) =>
profileUrl: performanceBarData.profileUrl, profileUrl: performanceBarData.profileUrl,
}; };
}, },
mounted() {
this.interceptor = PerformanceBarService.registerInterceptor(
this.peekUrl,
this.loadRequestDetails,
);
this.loadRequestDetails(this.requestId, window.location.href);
},
beforeDestroy() {
PerformanceBarService.removeInterceptor(this.interceptor);
},
methods: {
loadRequestDetails(requestId, requestUrl) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
}
this.store.addRequest(requestId, requestUrl);
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data.data);
})
.catch(() =>
Flash(`Error getting performance bar results for ${requestId}`),
);
},
},
render(createElement) { render(createElement) {
return createElement('performance-bar-app', { return createElement('performance-bar-app', {
props: { props: {
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
...@@ -43,7 +43,10 @@ export default { ...@@ -43,7 +43,10 @@ export default {
return this.isBranchTitleLong(this.mr.targetBranch); return this.isBranchTitleLong(this.mr.targetBranch);
}, },
webIdePath() { webIdePath() {
return webIDEUrl(this.mr.statusPath.replace('.json', '')); return mergeUrlParams({
target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
this.mr.targetProjectFullPath : '',
}, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
}, },
}, },
methods: { methods: {
......
...@@ -16,10 +16,11 @@ export default class MergeRequestStore { ...@@ -16,10 +16,11 @@ export default class MergeRequestStore {
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null; const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
this.squash = data.squash; this.squash = data.squash;
this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath || this.squashBeforeMergeHelpPath =
data.squash_before_merge_help_path; this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid;
this.title = data.title; this.title = data.title;
this.targetBranch = data.target_branch; this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch; this.sourceBranch = data.source_branch;
...@@ -85,6 +86,8 @@ export default class MergeRequestStore { ...@@ -85,6 +86,8 @@ export default class MergeRequestStore {
this.isMergeAllowed = data.mergeable || false; this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing; this.mergeOngoing = data.merge_ongoing;
this.allowCollaboration = data.allow_collaboration; this.allowCollaboration = data.allow_collaboration;
this.targetProjectFullPath = data.target_project_full_path;
this.sourceProjectFullPath = data.source_project_full_path;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
...@@ -97,7 +100,8 @@ export default class MergeRequestStore { ...@@ -97,7 +100,8 @@ export default class MergeRequestStore {
this.hasCI = data.has_ci; this.hasCI = data.has_ci;
this.ciStatus = data.ci_status; this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings'; this.isPipelinePassing =
this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped'; this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus; this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false; this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
......
...@@ -321,11 +321,18 @@ ...@@ -321,11 +321,18 @@
} }
&.activities { &.activities {
display: flex;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
overflow: hidden;
.nav-links { .nav-links {
border-bottom: 0; border-bottom: 0;
} }
@include media-breakpoint-down(xs) {
display: block;
overflow: visible;
}
} }
} }
......
...@@ -653,6 +653,7 @@ module Ci ...@@ -653,6 +653,7 @@ module Ci
variables.append(key: 'CI_JOB_NAME', value: name) variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage) variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_COMMIT_SHA', value: sha) variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref) variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug) variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag? variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
......
...@@ -368,8 +368,10 @@ class Project < ActiveRecord::Base ...@@ -368,8 +368,10 @@ class Project < ActiveRecord::Base
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600 chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
validates :build_timeout, allow_nil: true, validates :build_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600, numericality: { greater_than_or_equal_to: 10.minutes,
message: 'needs to be at least 10 minutes' } less_than: 1.month,
only_integer: true,
message: 'needs to be beetween 10 minutes and 1 month' }
# Returns a collection of projects that is either public or visible to the # Returns a collection of projects that is either public or visible to the
# logged in user. # logged in user.
......
...@@ -174,8 +174,8 @@ class Repository ...@@ -174,8 +174,8 @@ class Repository
CommitCollection.new(project, commits, ref) CommitCollection.new(project, commits, ref)
end end
def find_branch(name, fresh_repo: true) def find_branch(name)
raw_repository.find_branch(name, fresh_repo) raw_repository.find_branch(name)
end end
def find_tag(name) def find_tag(name)
......
...@@ -10,9 +10,15 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -10,9 +10,15 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_when_pipeline_succeeds expose :merge_when_pipeline_succeeds
expose :source_branch expose :source_branch
expose :source_project_id expose :source_project_id
expose :source_project_full_path do |merge_request|
merge_request.source_project&.full_path
end
expose :squash expose :squash
expose :target_branch expose :target_branch
expose :target_project_id expose :target_project_id
expose :target_project_full_path do |merge_request|
merge_request.project&.full_path
end
expose :allow_collaboration expose :allow_collaboration
expose :should_be_rebased?, as: :should_be_rebased expose :should_be_rebased?, as: :should_be_rebased
......
.nav-block.activities .nav-block.activities
= render 'shared/event_filter'
.controls .controls
= link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter'
.content_list .content_list
= spinner = spinner
.nav-block.activities .nav-block.activities
= render 'shared/event_filter'
.controls .controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter'
.content_list .content_list
= spinner = spinner
%div{ class: container_class } %div{ class: container_class }
.nav-block.activity-filter-block.activities .nav-block.activity-filter-block.activities
= render 'shared/event_filter'
.controls .controls
= link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn rss-btn has-tooltip' do = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn rss-btn has-tooltip' do
= icon('rss') = icon('rss')
= render 'shared/event_filter'
.content_list.project-activity{ :"data-href" => activity_project_path(@project) } .content_list.project-activity{ :"data-href" => activity_project_path(@project) }
= spinner = spinner
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.flex-fill
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs %ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
......
---
title: Fix RSS button interaction on Dashboard, Project and Group activities
merge_request: 20549
author:
type: fixed
---
title: Add missing predefined variable and fix docs
merge_request:
author:
type: fixed
---
title: Limit maximum project build timeout setting to 1 month
merge_request: 20591
author:
type: fixed
---
title: Clicking CI icon in Web IDE now opens up pipelines panel
merge_request:
author:
type: added
---
title: Avoid process deadlock in popen by consuming input pipes
merge_request: 20600
author:
type: fixed
---
title: Upgrade grape-path-helpers to 1.0.6
merge_request: 20601
author:
type: other
...@@ -47,6 +47,7 @@ future GitLab releases.** ...@@ -47,6 +47,7 @@ future GitLab releases.**
| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built | | **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. | | **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | | **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
| **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. |
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | | **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. | | **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message | | **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
...@@ -118,6 +119,7 @@ future GitLab releases.** ...@@ -118,6 +119,7 @@ future GitLab releases.**
| `CI_BUILD_ID` | `CI_JOB_ID` | | `CI_BUILD_ID` | `CI_JOB_ID` |
| `CI_BUILD_REF` | `CI_COMMIT_SHA` | | `CI_BUILD_REF` | `CI_COMMIT_SHA` |
| `CI_BUILD_TAG` | `CI_COMMIT_TAG` | | `CI_BUILD_TAG` | `CI_COMMIT_TAG` |
| `CI_BUILD_BEFORE_SHA` | `CI_COMMIT_BEFORE_SHA` |
| `CI_BUILD_REF_NAME` | `CI_COMMIT_REF_NAME` | | `CI_BUILD_REF_NAME` | `CI_COMMIT_REF_NAME` |
| `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` | | `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` |
| `CI_BUILD_NAME` | `CI_JOB_NAME` | | `CI_BUILD_NAME` | `CI_JOB_NAME` |
......
...@@ -364,12 +364,12 @@ When dragging issues between lists, different behavior occurs depending on the s ...@@ -364,12 +364,12 @@ When dragging issues between lists, different behavior occurs depending on the s
Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table: Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table:
| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Project Issue Boards | Configurable Group Issue Boards | Assignee Lists | Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| Core | 1 | 1 | No | No | No | | Core | 1 | 1 | No | No |
| Starter | Multiple | 1 | Yes | No | No | | Starter | Multiple | 1 | Yes | No |
| Premium | Multiple | Multiple | Yes | Yes | Yes | | Premium | Multiple | Multiple | Yes | Yes |
| Ultimate | Multiple | Multiple | Yes | Yes | Yes | | Ultimate | Multiple | Multiple | Yes | Yes |
## Tips ## Tips
......
...@@ -6,6 +6,18 @@ module API ...@@ -6,6 +6,18 @@ module API
before { authorize! :download_code, user_project } before { authorize! :download_code, user_project }
helpers do
def user_access
@user_access ||= Gitlab::UserAccess.new(current_user, project: user_project)
end
def authorize_push_to_branch!(branch)
unless user_access.can_push_to_branch?(branch)
forbidden!("You are not allowed to push into this branch")
end
end
end
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
...@@ -67,7 +79,7 @@ module API ...@@ -67,7 +79,7 @@ module API
optional :author_name, type: String, desc: 'Author name for commit' optional :author_name, type: String, desc: 'Author name for commit'
end end
post ':id/repository/commits' do post ':id/repository/commits' do
authorize! :push_code, user_project authorize_push_to_branch!(params[:branch])
attrs = declared_params attrs = declared_params
attrs[:branch_name] = attrs.delete(:branch) attrs[:branch_name] = attrs.delete(:branch)
...@@ -142,7 +154,7 @@ module API ...@@ -142,7 +154,7 @@ module API
requires :branch, type: String, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project authorize_push_to_branch!(params[:branch])
commit = user_project.commit(params[:sha]) commit = user_project.commit(params[:sha])
not_found!('Commit') unless commit not_found!('Commit') unless commit
......
...@@ -21,6 +21,10 @@ module Gitlab ...@@ -21,6 +21,10 @@ module Gitlab
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
stdout.set_encoding(Encoding::ASCII_8BIT) stdout.set_encoding(Encoding::ASCII_8BIT)
# stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
# Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
err_reader = Thread.new { stderr.read }
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
...@@ -32,7 +36,7 @@ module Gitlab ...@@ -32,7 +36,7 @@ module Gitlab
cmd_output << stdout.read cmd_output << stdout.read
end end
cmd_output << stderr.read cmd_output << err_reader.value
cmd_status = wait_thr.value.exitstatus cmd_status = wait_thr.value.exitstatus
end end
...@@ -55,16 +59,20 @@ module Gitlab ...@@ -55,16 +59,20 @@ module Gitlab
rerr, werr = IO.pipe rerr, werr = IO.pipe
pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true) pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true)
# stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
# Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
out_reader = Thread.new { rout.read }
err_reader = Thread.new { rerr.read }
begin begin
status = process_wait_with_timeout(pid, timeout)
# close write ends so we could read them # close write ends so we could read them
wout.close wout.close
werr.close werr.close
cmd_output = rout.readlines.join status = process_wait_with_timeout(pid, timeout)
cmd_output << rerr.readlines.join # Copying the behaviour of `popen` which merges stderr into output
cmd_output = out_reader.value
cmd_output << err_reader.value # Copying the behaviour of `popen` which merges stderr into output
[cmd_output, status.exitstatus] [cmd_output, status.exitstatus]
rescue Timeout::Error => e rescue Timeout::Error => e
......
...@@ -167,24 +167,9 @@ module Gitlab ...@@ -167,24 +167,9 @@ module Gitlab
# Directly find a branch with a simple name (e.g. master) # Directly find a branch with a simple name (e.g. master)
# #
# force_reload causes a new Rugged repository to be instantiated def find_branch(name)
# wrapped_gitaly_errors do
# This is to work around a bug in libgit2 that causes in-memory refs to gitaly_ref_client.find_branch(name)
# be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
def find_branch(name, force_reload = false)
gitaly_migrate(:find_branch) do |is_enabled|
if is_enabled
gitaly_ref_client.find_branch(name)
else
reload_rugged if force_reload
rugged_ref = rugged.branches[name]
if rugged_ref
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
end
end
end end
end end
...@@ -196,20 +181,8 @@ module Gitlab ...@@ -196,20 +181,8 @@ module Gitlab
# Returns the number of valid branches # Returns the number of valid branches
def branch_count def branch_count
gitaly_migrate(:branch_names) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_client.count_branch_names
gitaly_ref_client.count_branch_names
else
rugged.branches.each(:local).count do |ref|
begin
ref.name && ref.target # ensures the branch is valid
true
rescue Rugged::ReferenceError
false
end
end
end
end end
end end
...@@ -232,12 +205,8 @@ module Gitlab ...@@ -232,12 +205,8 @@ module Gitlab
# Returns the number of valid tags # Returns the number of valid tags
def tag_count def tag_count
gitaly_migrate(:tag_names) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_client.count_tag_names
gitaly_ref_client.count_tag_names
else
rugged.tags.count
end
end end
end end
...@@ -260,13 +229,8 @@ module Gitlab ...@@ -260,13 +229,8 @@ module Gitlab
# #
# Ref names must start with `refs/`. # Ref names must start with `refs/`.
def ref_exists?(ref_name) def ref_exists?(ref_name)
gitaly_migrate(:ref_exists, wrapped_gitaly_errors do
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| gitaly_ref_exists?(ref_name)
if is_enabled
gitaly_ref_exists?(ref_name)
else
rugged_ref_exists?(ref_name)
end
end end
end end
...@@ -274,12 +238,8 @@ module Gitlab ...@@ -274,12 +238,8 @@ module Gitlab
# #
# name - The name of the tag as a String. # name - The name of the tag as a String.
def tag_exists?(name) def tag_exists?(name)
gitaly_migrate(:ref_exists_tags, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_exists?("refs/tags/#{name}")
gitaly_ref_exists?("refs/tags/#{name}")
else
rugged_tag_exists?(name)
end
end end
end end
...@@ -287,12 +247,8 @@ module Gitlab ...@@ -287,12 +247,8 @@ module Gitlab
# #
# name - The name of the branch as a String. # name - The name of the branch as a String.
def branch_exists?(name) def branch_exists?(name)
gitaly_migrate(:ref_exists_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_exists?("refs/heads/#{name}")
gitaly_ref_exists?("refs/heads/#{name}")
else
rugged_branch_exists?(name)
end
end end
end end
...@@ -310,12 +266,8 @@ module Gitlab ...@@ -310,12 +266,8 @@ module Gitlab
end end
def delete_all_refs_except(prefixes) def delete_all_refs_except(prefixes)
gitaly_migrate(:ref_delete_refs) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
else
delete_refs(*all_ref_names_except(prefixes))
end
end end
end end
...@@ -714,25 +666,16 @@ module Gitlab ...@@ -714,25 +666,16 @@ module Gitlab
# Delete the specified branch from the repository # Delete the specified branch from the repository
def delete_branch(branch_name) def delete_branch(branch_name)
gitaly_migrate(:delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_client.delete_branch(branch_name)
gitaly_ref_client.delete_branch(branch_name)
else
rugged.branches.delete(branch_name)
end
end end
rescue Rugged::ReferenceError, CommandError => e rescue CommandError => e
raise DeleteBranchError, e raise DeleteBranchError, e
end end
def delete_refs(*ref_names) def delete_refs(*ref_names)
gitaly_migrate(:delete_refs, wrapped_gitaly_errors do
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| gitaly_delete_refs(*ref_names)
if is_enabled
gitaly_delete_refs(*ref_names)
else
git_delete_refs(*ref_names)
end
end end
end end
...@@ -742,12 +685,8 @@ module Gitlab ...@@ -742,12 +685,8 @@ module Gitlab
# create_branch("feature") # create_branch("feature")
# create_branch("other-feature", "master") # create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD") def create_branch(ref, start_point = "HEAD")
gitaly_migrate(:create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_ref_client.create_branch(ref, start_point)
gitaly_ref_client.create_branch(ref, start_point)
else
rugged_create_branch(ref, start_point)
end
end end
end end
...@@ -1175,7 +1114,7 @@ module Gitlab ...@@ -1175,7 +1114,7 @@ module Gitlab
end end
def can_be_merged?(source_sha, target_branch) def can_be_merged?(source_sha, target_branch)
if target_sha = find_branch(target_branch, true)&.target if target_sha = find_branch(target_branch)&.target
!gitaly_conflicts_client(source_sha, target_sha).conflicts? !gitaly_conflicts_client(source_sha, target_sha).conflicts?
else else
false false
...@@ -1546,17 +1485,6 @@ module Gitlab ...@@ -1546,17 +1485,6 @@ module Gitlab
end end
end end
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
def rugged_ref_exists?(ref_name)
raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/')
rugged.references.exist?(ref_name)
rescue Rugged::ReferenceError
false
end
# Returns true if the given ref name exists # Returns true if the given ref name exists
# #
# Ref names must start with `refs/`. # Ref names must start with `refs/`.
...@@ -1564,37 +1492,6 @@ module Gitlab ...@@ -1564,37 +1492,6 @@ module Gitlab
gitaly_ref_client.ref_exists?(ref_name) gitaly_ref_client.ref_exists?(ref_name)
end end
# Returns true if the given tag exists
#
# name - The name of the tag as a String.
def rugged_tag_exists?(name)
!!rugged.tags[name]
end
# Returns true if the given branch exists
#
# name - The name of the branch as a String.
def rugged_branch_exists?(name)
rugged.branches.exists?(name)
# If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
# Whatever code calls this method shouldn't have to deal with that so
# instead we just return `false` (which is true since a branch doesn't
# exist when it has an invalid name).
rescue Rugged::ReferenceError
false
end
def rugged_create_branch(ref, start_point)
rugged_ref = rugged.branches.create(ref, start_point)
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
rescue Rugged::ReferenceError => e
raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ %r{'refs/heads/#{ref}'}
raise InvalidRef.new("Invalid reference #{start_point}")
end
def gitaly_copy_gitattributes(revision) def gitaly_copy_gitattributes(revision)
gitaly_repository_client.apply_gitattributes(revision) gitaly_repository_client.apply_gitattributes(revision)
end end
...@@ -1687,20 +1584,6 @@ module Gitlab ...@@ -1687,20 +1584,6 @@ module Gitlab
remote_update(remote_name, url: url) remote_update(remote_name, url: url)
end end
def git_delete_refs(*ref_names)
instructions = ref_names.map do |ref|
"delete #{ref}\x00\x00"
end
message, status = run_git(%w[update-ref --stdin -z]) do |stdin|
stdin.write(instructions.join)
end
unless status.zero?
raise GitError.new("Could not delete refs #{ref_names}: #{message}")
end
end
def gitaly_delete_refs(*ref_names) def gitaly_delete_refs(*ref_names)
gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any? gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
end end
......
...@@ -4,10 +4,18 @@ module Gitlab ...@@ -4,10 +4,18 @@ module Gitlab
file_name_noext + '.log' file_name_noext + '.log'
end end
def self.debug(message)
build.debug(message)
end
def self.error(message) def self.error(message)
build.error(message) build.error(message)
end end
def self.warn(message)
build.warn(message)
end
def self.info(message) def self.info(message)
build.info(message) build.info(message)
end end
......
...@@ -34,11 +34,16 @@ module Gitlab ...@@ -34,11 +34,16 @@ module Gitlab
start = Time.now start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
# stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
# Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
out_reader = Thread.new { stdout.read }
err_reader = Thread.new { stderr.read }
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
cmd_stdout = stdout.read cmd_stdout = out_reader.value
cmd_stderr = stderr.read cmd_stderr = err_reader.value
cmd_status = wait_thr.value cmd_status = wait_thr.value
end end
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
"autosize": "^4.0.0", "autosize": "^4.0.0",
"axios": "^0.17.1", "axios": "^0.17.1",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-loader": "^7.1.4", "babel-loader": "^7.1.5",
"babel-plugin-transform-define": "^1.3.0", "babel-plugin-transform-define": "^1.3.0",
"babel-preset-latest": "^6.24.1", "babel-preset-latest": "^6.24.1",
"babel-preset-stage-2": "^6.24.1", "babel-preset-stage-2": "^6.24.1",
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"compression-webpack-plugin": "^1.1.11", "compression-webpack-plugin": "^1.1.11",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"cropper": "^2.3.0", "cropper": "^2.3.0",
"css-loader": "^0.28.11", "css-loader": "^1.0.0",
"d3-array": "^1.2.1", "d3-array": "^1.2.1",
"d3-axis": "^1.0.8", "d3-axis": "^1.0.8",
"d3-brush": "^1.0.4", "d3-brush": "^1.0.4",
...@@ -90,15 +90,15 @@ ...@@ -90,15 +90,15 @@
"url-loader": "^1.0.1", "url-loader": "^1.0.1",
"visibilityjs": "^1.2.4", "visibilityjs": "^1.2.4",
"vue": "^2.5.16", "vue": "^2.5.16",
"vue-loader": "^15.2.0", "vue-loader": "^15.2.4",
"vue-resource": "^1.5.0", "vue-resource": "^1.5.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.16", "vue-template-compiler": "^2.5.16",
"vue-virtual-scroll-list": "^1.2.5", "vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"webpack": "^4.11.1", "webpack": "^4.16.0",
"webpack-bundle-analyzer": "^2.11.1", "webpack-bundle-analyzer": "^2.13.1",
"webpack-cli": "^3.0.2", "webpack-cli": "^3.0.8",
"webpack-stats-plugin": "^0.2.1", "webpack-stats-plugin": "^0.2.1",
"worker-loader": "^2.0.0" "worker-loader": "^2.0.0"
}, },
...@@ -123,15 +123,16 @@ ...@@ -123,15 +123,16 @@
"ignore": "^3.3.7", "ignore": "^3.3.7",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"jasmine-core": "^2.9.0", "jasmine-core": "^2.9.0",
"jasmine-diff": "^0.1.3",
"jasmine-jquery": "^2.1.1", "jasmine-jquery": "^2.1.1",
"karma": "^2.0.2", "karma": "^2.0.4",
"karma-chrome-launcher": "^2.2.0", "karma-chrome-launcher": "^2.2.0",
"karma-coverage-istanbul-reporter": "^1.4.2", "karma-coverage-istanbul-reporter": "^1.4.2",
"karma-jasmine": "^1.1.1", "karma-jasmine": "^1.1.2",
"karma-mocha-reporter": "^2.2.5", "karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "3.0.0", "karma-webpack": "^4.0.0-beta.0",
"nodemon": "^1.17.3", "nodemon": "^1.18.2",
"prettier": "1.12.1", "prettier": "1.12.1",
"webpack-dev-server": "^3.1.4" "webpack-dev-server": "^3.1.4"
} }
......
...@@ -29,8 +29,10 @@ ...@@ -29,8 +29,10 @@
"merge_when_pipeline_succeeds": { "type": "boolean" }, "merge_when_pipeline_succeeds": { "type": "boolean" },
"source_branch": { "type": "string" }, "source_branch": { "type": "string" },
"source_project_id": { "type": "integer" }, "source_project_id": { "type": "integer" },
"source_project_full_path": { "type": ["string", "null"]},
"target_branch": { "type": "string" }, "target_branch": { "type": "string" },
"target_project_id": { "type": "integer" }, "target_project_id": { "type": "integer" },
"target_project_full_path": { "type": ["string", "null"]},
"allow_collaboration": { "type": "boolean"}, "allow_collaboration": { "type": "boolean"},
"metrics": { "metrics": {
"oneOf": [ "oneOf": [
......
...@@ -13,6 +13,7 @@ describe('ideStatusBar', () => { ...@@ -13,6 +13,7 @@ describe('ideStatusBar', () => {
store.state.currentProjectId = 'abcproject'; store.state.currentProjectId = 'abcproject';
store.state.projects.abcproject = projectData; store.state.projects.abcproject = projectData;
store.state.currentBranchId = 'master';
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store).$mount();
}); });
...@@ -60,4 +61,29 @@ describe('ideStatusBar', () => { ...@@ -60,4 +61,29 @@ describe('ideStatusBar', () => {
expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de'); expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
}); });
}); });
describe('pipeline status', () => {
it('opens right sidebar on clicking icon', done => {
spyOn(vm, 'setRightPane');
Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
details: {
status: {
text: 'success',
details_path: 'test',
icon: 'success',
},
},
});
vm
.$nextTick()
.then(() => {
vm.$el.querySelector('.ide-status-pipeline button').click();
expect(vm.setRightPane).toHaveBeenCalledWith('pipelines-list');
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -9,6 +9,9 @@ export const projectData = { ...@@ -9,6 +9,9 @@ export const projectData = {
master: { master: {
treeId: 'abcproject/master', treeId: 'abcproject/master',
can_push: true, can_push: true,
commit: {
id: '123',
},
}, },
}, },
mergeRequests: {}, mergeRequests: {},
......
import Vue from 'vue'; import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue'; import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store'; import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import MockAdapter from 'axios-mock-adapter';
describe('performance bar', () => { describe('performance bar app', () => {
let mock;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const store = new PerformanceBarStore(); const store = new PerformanceBarStore();
mock = new MockAdapter(axios);
mock.onGet('/-/peek/results').reply(
200,
{
data: {
gc: {
invokes: 0,
invoke_time: '0.00',
use_size: 0,
total_size: 0,
total_object: 0,
gc_time: '0.00',
},
host: { hostname: 'web-01' },
},
},
{},
);
vm = mountComponent(Vue.extend(performanceBarApp), { vm = mountComponent(Vue.extend(performanceBarApp), {
store, store,
env: 'development', env: 'development',
...@@ -45,44 +21,9 @@ describe('performance bar', () => { ...@@ -45,44 +21,9 @@ describe('performance bar', () => {
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
mock.restore();
}); });
it('sets the class to match the environment', () => { it('sets the class to match the environment', () => {
expect(vm.$el.getAttribute('class')).toContain('development'); expect(vm.$el.getAttribute('class')).toContain('development');
}); });
describe('loadRequestDetails', () => {
beforeEach(() => {
spyOn(vm.store, 'addRequest').and.callThrough();
});
it('does nothing if the request cannot be tracked', () => {
spyOn(vm.store, 'canTrackRequest').and.callFake(() => false);
vm.loadRequestDetails('123', 'https://gitlab.com/');
expect(vm.store.addRequest).not.toHaveBeenCalled();
});
it('adds the request immediately', () => {
vm.loadRequestDetails('123', 'https://gitlab.com/');
expect(vm.store.addRequest).toHaveBeenCalledWith(
'123',
'https://gitlab.com/',
);
});
it('makes an HTTP request for the request details', () => {
spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough();
vm.loadRequestDetails('456', 'https://gitlab.com/');
expect(PerformanceBarService.fetchRequestDetails).toHaveBeenCalledWith(
'/-/peek/results',
'456',
);
});
});
}); });
import axios from '~/lib/utils/axios_utils';
import performanceBar from '~/performance_bar';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
import MockAdapter from 'axios-mock-adapter';
describe('performance bar wrapper', () => {
let mock;
let vm;
beforeEach(() => {
const peekWrapper = document.createElement('div');
peekWrapper.setAttribute('id', 'js-peek');
peekWrapper.setAttribute('data-env', 'development');
peekWrapper.setAttribute('data-request-id', '123');
peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
document.body.appendChild(peekWrapper);
mock = new MockAdapter(axios);
mock.onGet('/-/peek/results').reply(
200,
{
data: {
gc: {
invokes: 0,
invoke_time: '0.00',
use_size: 0,
total_size: 0,
total_object: 0,
gc_time: '0.00',
},
host: { hostname: 'web-01' },
},
},
{},
);
vm = performanceBar({ container: '#js-peek' });
});
afterEach(() => {
vm.$destroy();
mock.restore();
});
describe('loadRequestDetails', () => {
beforeEach(() => {
spyOn(vm.store, 'addRequest').and.callThrough();
});
it('does nothing if the request cannot be tracked', () => {
spyOn(vm.store, 'canTrackRequest').and.callFake(() => false);
vm.loadRequestDetails('123', 'https://gitlab.com/');
expect(vm.store.addRequest).not.toHaveBeenCalled();
});
it('adds the request immediately', () => {
vm.loadRequestDetails('123', 'https://gitlab.com/');
expect(vm.store.addRequest).toHaveBeenCalledWith(
'123',
'https://gitlab.com/',
);
});
it('makes an HTTP request for the request details', () => {
spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough();
vm.loadRequestDetails('456', 'https://gitlab.com/');
expect(PerformanceBarService.fetchRequestDetails).toHaveBeenCalledWith(
'/-/peek/results',
'456',
);
});
});
});
...@@ -6,6 +6,7 @@ import '~/commons'; ...@@ -6,6 +6,7 @@ import '~/commons';
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import jasmineDiff from 'jasmine-diff';
import { getDefaultAdapter } from '~/lib/utils/axios_utils'; import { getDefaultAdapter } from '~/lib/utils/axios_utils';
import { FIXTURES_PATH, TEST_HOST } from './test_constants'; import { FIXTURES_PATH, TEST_HOST } from './test_constants';
...@@ -35,7 +36,15 @@ Vue.use(Translate); ...@@ -35,7 +36,15 @@ Vue.use(Translate);
jasmine.getFixtures().fixturesPath = FIXTURES_PATH; jasmine.getFixtures().fixturesPath = FIXTURES_PATH;
jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH; jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH;
beforeAll(() => jasmine.addMatchers(customMatchers)); beforeAll(() => {
jasmine.addMatchers(
jasmineDiff(jasmine, {
colors: true,
inline: true,
}),
);
jasmine.addMatchers(customMatchers);
});
// globalize common libraries // globalize common libraries
window.$ = $; window.$ = $;
......
...@@ -119,6 +119,7 @@ describe('MRWidgetHeader', () => { ...@@ -119,6 +119,7 @@ describe('MRWidgetHeader', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
mr: { mr: {
iid: 1,
divergedCommitsCount: 12, divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
...@@ -130,6 +131,8 @@ describe('MRWidgetHeader', () => { ...@@ -130,6 +131,8 @@ describe('MRWidgetHeader', () => {
emailPatchesPath: '/mr/email-patches', emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath', plainDiffPath: '/mr/plainDiffPath',
statusPath: 'abc', statusPath: 'abc',
sourceProjectFullPath: 'root/gitlab-ce',
targetProjectFullPath: 'gitlab-org/gitlab-ce',
}, },
}); });
}); });
...@@ -146,16 +149,40 @@ describe('MRWidgetHeader', () => { ...@@ -146,16 +149,40 @@ describe('MRWidgetHeader', () => {
const button = vm.$el.querySelector('.js-web-ide'); const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE'); expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); expect(button.getAttribute('href')).toEqual(
'/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=gitlab-org%2Fgitlab-ce',
);
}); });
it('renders web ide button with relative URL', () => { it('renders web ide button with blank query string if target & source project branch', done => {
vm.mr.targetProjectFullPath = 'root/gitlab-ce';
vm.$nextTick(() => {
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual(
'/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=',
);
done();
});
});
it('renders web ide button with relative URL', done => {
gon.relative_url_root = '/gitlab'; gon.relative_url_root = '/gitlab';
vm.mr.iid = 2;
const button = vm.$el.querySelector('.js-web-ide'); vm.$nextTick(() => {
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE'); expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); expect(button.getAttribute('href')).toEqual(
'/gitlab/-/ide/project/root/gitlab-ce/merge_requests/2?target_project=gitlab-org%2Fgitlab-ce',
);
done();
});
}); });
it('renders download dropdown with links', () => { it('renders download dropdown with links', () => {
......
...@@ -29,8 +29,10 @@ export default { ...@@ -29,8 +29,10 @@ export default {
source_branch: 'daaaa', source_branch: 'daaaa',
source_branch_link: 'daaaa', source_branch_link: 'daaaa',
source_project_id: 19, source_project_id: 19,
source_project_full_path: '/group1/project1',
target_branch: 'master', target_branch: 'master',
target_project_id: 19, target_project_id: 19,
target_project_full_path: '/group2/project2',
metrics: { metrics: {
merged_by: { merged_by: {
name: 'Administrator', name: 'Administrator',
......
...@@ -2,6 +2,9 @@ require 'spec_helper' ...@@ -2,6 +2,9 @@ require 'spec_helper'
describe 'Gitlab::Git::Popen' do describe 'Gitlab::Git::Popen' do
let(:path) { Rails.root.join('tmp').to_s } let(:path) { Rails.root.join('tmp').to_s }
let(:test_string) { 'The quick brown fox jumped over the lazy dog' }
# The pipe buffer is typically 64K. This string is about 440K.
let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] }
let(:klass) do let(:klass) do
Class.new(Object) do Class.new(Object) do
...@@ -70,6 +73,15 @@ describe 'Gitlab::Git::Popen' do ...@@ -70,6 +73,15 @@ describe 'Gitlab::Git::Popen' do
end end
end end
end end
context 'with a process that writes a lot of data to stderr' do
it 'returns zero' do
output, status = klass.new.popen(spew_command, path)
expect(output).to include(test_string)
expect(status).to eq(0)
end
end
end end
context 'popen_with_timeout' do context 'popen_with_timeout' do
...@@ -85,6 +97,17 @@ describe 'Gitlab::Git::Popen' do ...@@ -85,6 +97,17 @@ describe 'Gitlab::Git::Popen' do
it { expect(output).to include('tests') } it { expect(output).to include('tests') }
end end
context 'multi-line string' do
let(:test_string) { "this is 1 line\n2nd line\n3rd line\n" }
let(:result) { klass.new.popen_with_timeout(['echo', test_string], timeout, path) }
let(:output) { result.first }
let(:status) { result.last }
it { expect(status).to be_zero }
# echo adds its own line
it { expect(output).to eq(test_string + "\n") }
end
context 'non-zero status' do context 'non-zero status' do
let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) } let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) }
let(:output) { result.first } let(:output) { result.first }
...@@ -110,6 +133,13 @@ describe 'Gitlab::Git::Popen' do ...@@ -110,6 +133,13 @@ describe 'Gitlab::Git::Popen' do
it "handles processes that do not shutdown correctly" do it "handles processes that do not shutdown correctly" do
expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error)
end end
it 'handles process that writes a lot of data to stderr' do
output, status = klass.new.popen_with_timeout(spew_command, timeout, path)
expect(output).to include(test_string)
expect(status).to eq(0)
end
end end
context 'timeout period' do context 'timeout period' do
......
...@@ -1187,50 +1187,17 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1187,50 +1187,17 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
describe '#find_branch' do describe '#find_branch' do
shared_examples 'finding a branch' do it 'should return a Branch for master' do
it 'should return a Branch for master' do branch = repository.find_branch('master')
branch = repository.find_branch('master')
expect(branch).to be_a_kind_of(Gitlab::Git::Branch) expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master') expect(branch.name).to eq('master')
end
it 'should handle non-existent branch' do
branch = repository.find_branch('this-is-garbage')
expect(branch).to eq(nil)
end
end end
context 'when Gitaly find_branch feature is enabled' do it 'should handle non-existent branch' do
it_behaves_like 'finding a branch' branch = repository.find_branch('this-is-garbage')
end
context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
context 'force_reload is true' do
it 'should reload Rugged::Repository' do
expect(Rugged::Repository).to receive(:new).twice.and_call_original
repository.find_branch('master')
branch = repository.find_branch('master', force_reload: true)
expect(branch).to be_a_kind_of(Gitlab::Git::Branch) expect(branch).to eq(nil)
expect(branch.name).to eq('master')
end
end
context 'force_reload is false' do
it 'should not reload Rugged::Repository' do
expect(Rugged::Repository).to receive(:new).once.and_call_original
branch = repository.find_branch('master', force_reload: false)
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
end
end end
end end
......
...@@ -55,6 +55,19 @@ describe Gitlab::Popen do ...@@ -55,6 +55,19 @@ describe Gitlab::Popen do
end end
end end
context 'with a process that writes a lot of data to stderr' do
let(:test_string) { 'The quick brown fox jumped over the lazy dog' }
# The pipe buffer is typically 64K. This string is about 440K.
let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] }
it 'returns zero' do
output, status = @klass.new.popen(spew_command, path)
expect(output).to include(test_string)
expect(status).to eq(0)
end
end
context 'without a directory argument' do context 'without a directory argument' do
before do before do
@output, @status = @klass.new.popen(%w(ls)) @output, @status = @klass.new.popen(%w(ls))
......
...@@ -1613,6 +1613,7 @@ describe Ci::Build do ...@@ -1613,6 +1613,7 @@ describe Ci::Build do
{ key: 'CI_JOB_NAME', value: 'test', public: true }, { key: 'CI_JOB_NAME', value: 'test', public: true },
{ key: 'CI_JOB_STAGE', value: 'test', public: true }, { key: 'CI_JOB_STAGE', value: 'test', public: true },
{ key: 'CI_COMMIT_SHA', value: build.sha, public: true }, { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
{ key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true }, { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true }, { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
{ key: 'CI_BUILD_REF', value: build.sha, public: true }, { key: 'CI_BUILD_REF', value: build.sha, public: true },
......
...@@ -149,23 +149,25 @@ describe Project do ...@@ -149,23 +149,25 @@ describe Project do
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_length_of(:path).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(2000) } it { is_expected.to validate_length_of(:description).is_at_most(2000) }
it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) } it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) }
it { is_expected.to allow_value('').for(:ci_config_path) } it { is_expected.to allow_value('').for(:ci_config_path) }
it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) } it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) }
it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) } it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
it { is_expected.to validate_presence_of(:creator) } it { is_expected.to validate_presence_of(:creator) }
it { is_expected.to validate_presence_of(:namespace) } it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:repository_storage) } it { is_expected.to validate_presence_of(:repository_storage) }
it 'validates build timeout constraints' do
is_expected.to validate_numericality_of(:build_timeout)
.only_integer
.is_greater_than_or_equal_to(10.minutes)
.is_less_than(1.month)
.with_message('needs to be beetween 10 minutes and 1 month')
end
it 'does not allow new projects beyond user limits' do it 'does not allow new projects beyond user limits' do
project2 = build(:project) project2 = build(:project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
......
...@@ -1022,24 +1022,6 @@ describe Repository do ...@@ -1022,24 +1022,6 @@ describe Repository do
end end
end end
describe '#find_branch' do
context 'fresh_repo is true' do
it 'delegates the call to raw_repository' do
expect(repository.raw_repository).to receive(:find_branch).with('master', true)
repository.find_branch('master', fresh_repo: true)
end
end
context 'fresh_repo is false' do
it 'delegates the call to raw_repository' do
expect(repository.raw_repository).to receive(:find_branch).with('master', false)
repository.find_branch('master', fresh_repo: false)
end
end
end
describe '#update_branch_with_hooks' do describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
......
...@@ -514,6 +514,38 @@ describe API::Commits do ...@@ -514,6 +514,38 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end end
end end
context 'when committing into a fork as a maintainer' do
include_context 'merge request allowing collaboration'
let(:project_id) { forked_project.id }
def push_params(branch_name)
{
branch: branch_name,
commit_message: 'Hello world',
actions: [
{
action: 'create',
file_path: 'foo/bar/baz.txt',
content: 'puts 8'
}
]
}
end
it 'allows pushing to the source branch of the merge request' do
post api(url, user), push_params('feature')
expect(response).to have_gitlab_http_status(:created)
end
it 'denies pushing to another branch' do
post api(url, user), push_params('other-branch')
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end end
describe 'GET /projects/:id/repository/commits/:sha/refs' do describe 'GET /projects/:id/repository/commits/:sha/refs' do
...@@ -1065,11 +1097,29 @@ describe API::Commits do ...@@ -1065,11 +1097,29 @@ describe API::Commits do
it 'returns 400 if you are not allowed to push to the target branch' do it 'returns 400 if you are not allowed to push to the target branch' do
post api(route, current_user), branch: 'feature' post api(route, current_user), branch: 'feature'
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('You are not allowed to push into this branch') expect(json_response['message']).to match(/You are not allowed to push into this branch/)
end end
end end
end end
context 'when cherry picking to a fork as a maintainer' do
include_context 'merge request allowing collaboration'
let(:project_id) { forked_project.id }
it 'allows access from a maintainer that to the source branch' do
post api(route, user), branch: 'feature'
expect(response).to have_gitlab_http_status(:created)
end
it 'denies cherry picking to another branch' do
post api(route, user), branch: 'master'
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end end
describe 'POST /projects/:id/repository/commits/:sha/comments' do describe 'POST /projects/:id/repository/commits/:sha/comments' do
......
...@@ -11,6 +11,21 @@ describe MergeRequestWidgetEntity do ...@@ -11,6 +11,21 @@ describe MergeRequestWidgetEntity do
described_class.new(resource, request: request).as_json described_class.new(resource, request: request).as_json
end end
describe 'source_project_full_path' do
it 'includes the full path of the source project' do
expect(subject[:source_project_full_path]).to be_present
end
context 'when the source project is missing' do
it 'returns `nil` for the source project' do
resource.allow_broken = true
resource.update!(source_project: nil)
expect(subject[:source_project_full_path]).to be_nil
end
end
end
describe 'pipeline' do describe 'pipeline' do
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) } let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
......
shared_context 'merge request allowing collaboration' do
include ProjectForksHelper
let(:canonical) { create(:project, :public, :repository) }
let(:forked_project) { fork_project(canonical, nil, repository: true) }
before do
canonical.add_maintainer(user)
create(:merge_request,
target_project: canonical,
source_project: forked_project,
source_branch: 'feature',
allow_collaboration: true)
end
end
This diff is collapsed.
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