Commit 9e7f82d3 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into '1790-remove-repository-storage-from-settings-api'

# Conflicts:
#   doc/api/v3_to_v4.md
parents 10788287 9f9ed778
......@@ -6,8 +6,6 @@
/* global AwardsHandler */
/* global Aside */
function requireAll(context) { return context.keys().map(context); }
window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete');
require('jquery-ui/ui/draggable');
......@@ -48,18 +46,186 @@ require('./shortcuts_issuable');
require('./shortcuts_network');
require('vendor/jquery.nicescroll');
require('./geo/geo_bundle');
requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
// behaviors
require('./behaviors/autosize');
require('./behaviors/details_behavior');
require('./behaviors/quick_submit');
require('./behaviors/requires_input');
require('./behaviors/toggler_behavior');
// blob
require('./blob/blob_ci_yaml');
require('./blob/blob_dockerfile_selector');
require('./blob/blob_dockerfile_selectors');
require('./blob/blob_file_dropzone');
require('./blob/blob_gitignore_selector');
require('./blob/blob_gitignore_selectors');
require('./blob/blob_license_selector');
require('./blob/blob_license_selectors');
require('./blob/template_selector');
// templates
require('./templates/issuable_template_selector');
require('./templates/issuable_template_selectors');
// commit
require('./commit/file.js');
require('./commit/image_file.js');
// extensions
require('./extensions/array');
require('./extensions/custom_event');
require('./extensions/element');
require('./extensions/jquery');
require('./extensions/object');
// lib/utils
require('./lib/utils/animate');
require('./lib/utils/bootstrap_linked_tabs');
require('./lib/utils/common_utils');
require('./lib/utils/datetime_utility');
require('./lib/utils/notify');
require('./lib/utils/pretty_time');
require('./lib/utils/text_utility');
require('./lib/utils/type_utility');
require('./lib/utils/url_utility');
// u2f
require('./u2f/authenticate');
require('./u2f/error');
require('./u2f/register');
require('./u2f/util');
// droplab
require('./droplab/droplab');
require('./droplab/droplab_ajax');
require('./droplab/droplab_ajax_filter');
require('./droplab/droplab_filter');
// everything else
require('./abuse_reports');
require('./activities');
require('./admin');
require('./api');
require('./aside');
require('./autosave');
require('./awards_handler');
require('./breakpoints');
require('./broadcast_message');
require('./build');
require('./build_artifacts');
require('./build_variables');
require('./ci_lint_editor');
require('./commit');
require('./commits');
require('./compare');
require('./compare_autocomplete');
require('./confirm_danger_modal');
require('./copy_as_gfm');
require('./copy_to_clipboard');
require('./create_label');
require('./diff');
require('./dispatcher');
require('./dropzone_input');
require('./due_date_select');
require('./files_comment_button');
require('./flash');
require('./gfm_auto_complete');
require('./gl_dropdown');
require('./gl_field_error');
require('./gl_field_errors');
require('./gl_form');
require('./group_avatar');
require('./group_label_subscription');
require('./groups_select');
require('./header');
require('./importer_status');
require('./issuable');
require('./issuable_context');
require('./issuable_form');
require('./issue');
require('./issue_status_select');
require('./issues_bulk_assignment');
require('./label_manager');
require('./labels');
require('./labels_select');
require('./layout_nav');
require('./line_highlighter');
require('./logo');
require('./member_expiration_date');
require('./members');
require('./merge_request');
require('./merge_request_tabs');
require('./merge_request_widget');
require('./merged_buttons');
require('./milestone');
require('./milestone_select');
require('./mini_pipeline_graph_dropdown');
require('./namespace_select');
require('./new_branch_form');
require('./new_commit_form');
require('./notes');
require('./notifications_dropdown');
require('./notifications_form');
require('./pager');
require('./pipelines');
require('./preview_markdown');
require('./project');
require('./project_avatar');
require('./project_find_file');
require('./project_fork');
require('./project_import');
require('./project_label_subscription');
require('./project_new');
require('./project_select');
require('./project_show');
require('./project_variables');
require('./projects_list');
require('./render_gfm');
require('./render_math');
require('./right_sidebar');
require('./search');
require('./search_autocomplete');
require('./shortcuts');
require('./shortcuts_blob');
require('./shortcuts_dashboard_navigation');
require('./shortcuts_find_file');
require('./shortcuts_issuable');
require('./shortcuts_navigation');
require('./shortcuts_network');
require('./signin_tabs_memoizer');
require('./single_file_diff');
require('./smart_interval');
require('./snippets_list');
require('./star');
require('./subbable_resource');
require('./subscription');
require('./subscription_select');
require('./syntax_highlight');
require('./task_list');
require('./todos');
require('./tree');
require('./user');
require('./user_tabs');
require('./username_validator');
require('./users_select');
require('./version_check_image');
require('./visibility_select');
require('./wikis');
require('./zen_mode');
require('vendor/fuzzaldrin-plus');
require('es6-promise').polyfill();
// EE-only scripts
require('./admin_email_select');
require('./application_settings');
require('./approvals');
require('./ldap_groups_select');
require('./path_locks');
require('./weight_select');
(function () {
document.addEventListener('beforeunload', function () {
// Unbind scroll events
......
/* eslint-disable no-new, import/first */
/**
* Renders a deploy board.
*
* A deploy board is composed by:
* - Information area with percentage of completion.
* - Instances with status.
* - Button Actions.
* [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*
* The data of each deploy board needs to be fetched when we render the component.
*
* The endpoint response can sometimes be 204, in those cases we need to retry the request.
* This should be done using backoff pooling and we should make no more than 3 request
* for each deploy board.
* After the third request we need to show a message saying we can't fetch the data.
* Please refer to this [comment](https://gitlab.com/gitlab-org/gitlab-ee/issues/1589#note_23630610)
* for more information
*/
const instanceComponent = require('./deploy_board_instance_component.js.es6');
const statusCodes = require('~/lib/utils/http_status');
const Flash = require('~/flash');
require('~/lib/utils/common_utils.js.es6');
module.exports = {
components: {
instanceComponent,
},
props: {
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
deployBoardData: {
type: Object,
required: true,
},
environmentID: {
type: Number,
required: true,
},
},
data() {
return {
isLoading: false,
hasError: false,
backOffRequestCounter: 0,
};
},
created() {
this.isLoading = true;
const maxNumberOfRequests = 3;
// If the response is 204, we make 3 more requests.
gl.utils.backOff((next, stop) => {
this.service.getDeployBoard(this.environmentID)
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else {
stop(resp);
}
} else {
stop(resp);
}
})
.catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.hasError = true;
return resp;
}
return resp.json();
})
.then((response) => {
this.store.storeDeployBoard(this.environmentID, response);
return response;
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the deploy board.', 'alert');
});
},
computed: {
canRenderDeployBoard() {
return !this.isLoading && !this.hasError && Object.keys(this.deployBoardData).length;
},
instanceTitle() {
let title;
if (this.deployBoardData.instances.length === 1) {
title = 'Instance';
} else {
title = 'Instances';
}
return title;
},
},
template: `
<div class="js-deploy-board deploy-board">
<div v-if="isLoading">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div v-if="canRenderDeployBoard">
<section class="deploy-board-information">
<span>
<span class="percentage">{{deployBoardData.completion}}%</span>
<span class="text">Complete</span>
</span>
</section>
<section class="deploy-board-instances">
<p class="text">{{instanceTitle}}</p>
<div class="deploy-board-instances-container">
<template v-for="instance in deployBoardData.instances">
<instance-component
:status="instance.status"
:tooltipText="instance.tooltip">
</instance-component>
</template>
</div>
</section>
<section class="deploy-board-actions">
<a class="btn"
data-method="post"
rel="nofollow"
v-if="deployBoardData.rollback_url"
:href="deployBoardData.rollback_url">
Rollback
</a>
<a class="btn btn-red btn-inverted"
data-method="post"
rel="nofollow"
v-if="deployBoardData.abort_url"
:href="deployBoardData.abort_url">
Abort
</a>
</section>
</div>
<div v-if="!isLoading && hasError" class="deploy-board-error-message">
We can't fetch the data right now. Please try again later.
</div>
</div>
`,
};
/**
* An instance in deploy board is represented by a square in this mockup:
* https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png
*
* Each instance has a state and a tooltip.
* The state needs to be represented in different colors,
* see more information about this in https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png
*
*/
module.exports = {
props: {
/**
* Represents the status of the pod. Each state is represented with a different
* color.
* It should be one of the following:
* finished || deploying || failed || ready || preparing || waiting
*/
status: {
type: String,
required: true,
default: 'finished',
},
tooltipText: {
type: String,
required: false,
default: '',
},
},
computed: {
cssClass() {
return `deploy-board-instance-${this.status}`;
},
},
template: `
<div
class="deploy-board-instance has-tooltip"
:class="cssClass"
:data-title="tooltipText"
data-toggle="tooltip"
data-placement="top">
</div>
`,
};
......@@ -3,12 +3,12 @@
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentsService = require('~/environments/services/environments_service');
const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
const EnvironmentsStore = require('~/environments/stores/environments_store');
require('~/vue_shared/components/table_pagination');
require('~/lib/utils/common_utils');
require('~/vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-component', {
......@@ -23,6 +23,7 @@ module.exports = Vue.component('environment-component', {
return {
store,
service: {},
state: store.state,
visibility: 'available',
isLoading: false,
......@@ -62,6 +63,16 @@ module.exports = Vue.component('environment-component', {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
/**
* Pagination should only be rendered when we have information about it and when the
* number of total pages is bigger than 1.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return this.state.paginationInformation && this.state.paginationInformation.totalPages > 1;
},
},
/**
......@@ -74,11 +85,11 @@ module.exports = Vue.component('environment-component', {
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
......@@ -99,8 +110,15 @@ module.exports = Vue.component('environment-component', {
},
methods: {
toggleRow(model) {
return this.store.toggleFolder(model.name);
/**
* Toggles the visibility of the deploy boards of the clicked environment.
*
* @param {Object} model
* @return {Object}
*/
toggleDeployBoard(model) {
return this.store.toggleDeployBoard(model.id);
},
/**
......@@ -179,10 +197,13 @@ module.exports = Vue.component('environment-component', {
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard"
:store="store"
:service="service">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
<table-pagination v-if="shouldRenderPagination"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
......
/**
* Environment Item Component
*
* Renders a table row for each environment.
*/
const Vue = require('vue');
const Timeago = require('timeago.js');
......@@ -9,12 +15,6 @@ const StopComponent = require('./environment_stop');
const RollbackComponent = require('./environment_rollback');
const TerminalButtonComponent = require('./environment_terminal_button');
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
const timeagoInstance = new Timeago();
module.exports = Vue.component('environment-item', {
......@@ -61,11 +61,16 @@ module.exports = Vue.component('environment-item', {
type: String,
required: false,
},
toggleDeployBoard: {
type: Function,
required: false,
},
},
computed: {
/**
* Verifies if `last_deployment` key exists in the current Envrionment.
* Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has
* an helper.
*
......@@ -414,7 +419,6 @@ module.exports = Vue.component('environment-item', {
folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`;
},
},
/**
......@@ -435,11 +439,27 @@ module.exports = Vue.component('environment-item', {
template: `
<tr>
<td>
<span class="deploy-board-icon"
v-if="model.hasDeployBoard"
@click="toggleDeployBoard(model)">
<i v-show="!model.isDeployBoardVisible"
class="fa fa-caret-right"
aria-hidden="true">
</i>
<i v-show="model.isDeployBoardVisible"
class="fa fa-caret-down"
aria-hidden="true">
</i>
</span>
<a v-if="!model.isFolder"
class="environment-name"
:href="environmentPath">
{{model.name}}
</a>
<a v-else class="folder-name" :href="folderUrl">
<span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i>
......
/**
* Render environments table.
*
* Dumb component used to render top level environments and
* the folder view.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
const DeployBoard = require('./deploy_board_component');
module.exports = Vue.component('environment-table-component', {
components: {
'environment-item': EnvironmentItem,
EnvironmentItem,
DeployBoard,
},
props: {
......@@ -43,6 +48,24 @@ module.exports = Vue.component('environment-table-component', {
type: String,
required: false,
},
toggleDeployBoard: {
type: Function,
required: false,
default: () => {},
},
store: {
type: Object,
required: false,
default: () => ({}),
},
service: {
type: Object,
required: false,
default: () => ({}),
},
},
template: `
......@@ -60,13 +83,26 @@ module.exports = Vue.component('environment-table-component', {
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard"></tr>
<tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<td colspan="6" class="deploy-board-container">
<deploy-board
:store="store"
:service="service"
:environmentID="model.id"
:deployBoardData="model.deployBoardData">
</deploy-board>
</td>
</tr>
</template>
</tbody>
</table>
......
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
/* eslint-disable no-new */
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
const EnvironmentsService = require('~/environments//services/environments_service');
const EnvironmentTable = require('~/environments/components/environments_table');
const EnvironmentsStore = require('~/environments//stores/environments_store');
const Flash = require('~/flash');
require('~/vue_shared/components/table_pagination');
require('~/lib/utils/common_utils');
require('~/vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-folder-view', {
......@@ -26,6 +26,7 @@ module.exports = Vue.component('environment-folder-view', {
return {
store,
service: {},
folderName,
endpoint,
state: store.state,
......@@ -88,11 +89,11 @@ module.exports = Vue.component('environment-folder-view', {
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
......@@ -113,6 +114,17 @@ module.exports = Vue.component('environment-folder-view', {
},
methods: {
/**
* Toggles the visibility of the deploy boards of the clicked environment.
*
* @param {Object} model
* @return {Object}
*/
toggleDeployBoard(model) {
return this.store.toggleDeployBoard(model.id);
},
/**
* Will change the page number and update the URL.
*
......@@ -168,7 +180,10 @@ module.exports = Vue.component('environment-folder-view', {
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard"
:store="store"
:service="service">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
......@@ -3,11 +3,17 @@ const Vue = require('vue');
class EnvironmentsService {
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
this.deployBoard = Vue.resource('environments/{id}/status.json');
}
all() {
get() {
return this.environments.get();
}
getDeployBoard(environmentID) {
return this.deployBoard.get({ id: environmentID });
}
}
module.exports = EnvironmentsService;
......@@ -30,6 +30,14 @@ class EnvironmentsStore {
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
*
* Top level environments - when the size is 1 - with `rollout_status_path`
* can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData`
* keys to those environments.
* The first key will let's us know if we should or not render the deploy board.
* It will be toggled when the user clicks to seee the deploy board.
*
* The second key will allow us to update the environment with the received deploy board data.
*
* @param {Array} environments
* @returns {Array}
*/
......@@ -37,15 +45,21 @@ class EnvironmentsStore {
const filteredEnvironments = environments.map((env) => {
let filtered = {};
if (env.size > 1) {
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
}
if (env.latest) {
filtered = Object.assign(filtered, env, env.latest);
filtered = Object.assign({}, env, env.latest);
delete filtered.latest;
} else {
filtered = Object.assign(filtered, env);
filtered = Object.assign({}, env);
}
if (filtered.size > 1) {
filtered = Object.assign(filtered, env, { isFolder: true, folderName: env.name });
} else if (filtered.size === 1 && filtered.rollout_status_path) {
filtered = Object.assign(filtered, env, {
hasDeployBoard: true,
isDeployBoardVisible: false,
deployBoardData: {},
});
}
return filtered;
......@@ -56,6 +70,20 @@ class EnvironmentsStore {
return filteredEnvironments;
}
/**
* Stores the pagination information needed to render the pagination for the
* table.
*
* Normalizes the headers to uppercase since they can be provided either
* in uppercase or lowercase.
*
* Parses to an integer the normalized ones needed for the pagination component.
*
* Stores the normalized and parsed information.
*
* @param {Object} pagination = {}
* @return {Object}
*/
setPagination(pagination = {}) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
......@@ -85,6 +113,48 @@ class EnvironmentsStore {
this.state.stoppedCounter = count;
return count;
}
/**
* Toggles deploy board visibility for the provided environment ID.
*
* @param {Object} environment
* @return {Array}
*/
toggleDeployBoard(environmentID) {
const environments = this.state.environments.slice();
this.state.environments = environments.map((env) => {
let updated = Object.assign({}, env);
if (env.id === environmentID) {
updated = Object.assign({}, updated, { isDeployBoardVisible: !env.isDeployBoardVisible });
}
return updated;
});
return this.state.environments;
}
/**
* Store deploy board data for given environment.
*
* @param {Number} environmentID
* @param {Object} deployBoard
* @return {Array}
*/
storeDeployBoard(environmentID, deployBoard) {
const environments = Object.assign([], this.state.environments);
this.state.environments = environments.map((env) => {
let updated = Object.assign({}, env);
if (env.id === environmentID) {
updated = Object.assign({}, updated, { deployBoardData: deployBoard });
}
return updated;
});
return this.state.environments;
}
}
module.exports = EnvironmentsStore;
......@@ -296,5 +296,57 @@
* @returns {Boolean}
*/
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
/**
* Back Off exponential algorithm
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
*
* @param {Function<next, stop>} fn function to be called
* @param {Number} timeout
* @return {Promise<Any, Error>}
* @example
* ```
* backOff(function (next, stop) {
* // Let's perform this function repeatedly for 60s or for the timeout provided.
*
* ourFunction()
* .then(function (result) {
* // continue if result is not what we need
* next();
*
* // when result is what we need let's stop with the repetions and jump out of the cycle
* stop(result);
* })
* .catch(function (error) {
* // if there is an error, we need to stop this with an error.
* stop(error);
* })
* }, 60000)
* .then(function (result) {})
* .catch(function (error) {
* // deal with errors passed to stop()
* })
* ```
*/
w.gl.utils.backOff = (fn, timeout = 60000) => {
let nextInterval = 2000;
const startTime = (+new Date());
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => {
if (new Date().getTime() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), nextInterval);
nextInterval *= 2;
} else {
reject(new Error('BACKOFF_TIMEOUT'));
}
};
fn(next, stop);
});
};
})(window);
}).call(window);
/**
* exports HTTP status codes
*/
const statusCodes = {
NO_CONTENT: 204,
OK: 200,
};
export default statusCodes;
......@@ -160,3 +160,117 @@
}
}
}
/**
* Deploy boards
*/
.deploy-board > div {
display: flex;
justify-content: space-between;
.deploy-board-information {
order: 1;
display: flex;
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 20px 10px;
> span {
text-align: center;
}
.percentage {
color: $gl-text-color;
}
.text {
color: $gl-text-color-secondary;
}
}
.deploy-board-instances {
order: 2;
width: 75%;
.text {
color: $gl-text-color-secondary;
font-size: 12px;
}
.deploy-board-instances-container {
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin-top: -8px;
}
}
.deploy-board-actions {
order: 3;
align-self: center;
}
&.deploy-board-error-message {
justify-content: center;
}
}
.deploy-board-instance {
width: 15px;
height: 15px;
border-radius: 3px;
border-width: 1px;
border-style: solid;
margin: 1px;
&-finished {
background-color: lighten($green-light, 25%);
border-color: $green-light;
}
&-deploying {
background-color: lighten($green-light, 40%);
border-color: $green-light;
}
&-failed {
background-color: lighten($red-light, 20%);
border-color: $red-normal;
}
&-ready {
background-color: lighten($border-color, 1%);
border-color: $border-color;
}
&-preparing {
background-color: lighten($border-color, 5%);
border-color: $border-color;
}
&-waiting {
background-color: $white-light;
border-color: $border-color;
}
}
.deploy-board-icon i {
cursor: pointer;
color: $layout-link-gray;
padding-right: 10px;
}
.deploy-board {
padding: 10px;
background-color: $gray-light;
min-height: 20px;
.fa-spinner {
margin: 0 auto;
width: 20px;
display: block;
font-size: 20px;
}
}
......@@ -98,6 +98,10 @@
padding: 10px 0;
}
td.deploy-board-container {
padding: 0;
}
.commit-link {
padding: 9px 8px 10px;
}
......
......@@ -762,136 +762,63 @@ class Repository
@tags ||= raw_repository.tags
end
# rubocop:disable Metrics/ParameterLists
def commit_dir(
user, path,
message:, branch_name:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
check_tree_entry_for_dir(branch_name, path)
if start_branch_name
start_project.repository.
check_tree_entry_for_dir(start_branch_name, path)
end
def create_dir(user, path, **options)
options[:user] = user
options[:actions] = [{ action: :create_dir, file_path: path }]
commit_file(
user,
"#{path}/.gitkeep",
'',
message: message,
branch_name: branch_name,
update: false,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project)
multi_action(**options)
end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists
def commit_file(
user, path, content,
message:, branch_name:, update: true,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
unless update
error_message = "Filename already exists; update not allowed"
def create_file(user, path, content, **options)
options[:user] = user
options[:actions] = [{ action: :create, file_path: path, content: content }]
if tree_entry_at(branch_name, path)
raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
end
multi_action(**options)
end
if start_branch_name &&
start_project.repository.tree_entry_at(start_branch_name, path)
raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
end
end
def update_file(user, path, content, **options)
previous_path = options.delete(:previous_path)
action = previous_path && previous_path != path ? :move : :update
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project,
actions: [{ action: :create,
file_path: path,
content: content }])
end
# rubocop:enable Metrics/ParameterLists
options[:user] = user
options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
# rubocop:disable Metrics/ParameterLists
def update_file(
user, path, content,
message:, branch_name:, previous_path:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
action = if previous_path && previous_path != path
:move
else
:update
end
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project,
actions: [{ action: action,
file_path: path,
content: content,
previous_path: previous_path }])
multi_action(**options)
end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists
def remove_file(
user, path,
message:, branch_name:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project,
actions: [{ action: :delete,
file_path: path }])
def delete_file(user, path, **options)
options[:user] = user
options[:actions] = [{ action: :delete, file_path: path }]
multi_action(**options)
end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_project: start_project) do |start_commit|
index = rugged.index
parents = if start_commit
index.read_tree(start_commit.raw_commit.tree)
[start_commit.sha]
else
[]
end
index = Gitlab::Git::Index.new(raw_repository)
actions.each do |act|
git_action(index, act)
if start_commit
index.read_tree(start_commit.raw_commit.tree)
parents = [start_commit.sha]
else
parents = []
end
actions.each do |options|
index.public_send(options.delete(:action), options)
end
options = {
tree: index.write_tree(rugged),
tree: index.write_tree,
message: message,
parents: parents
}
......@@ -1255,30 +1182,6 @@ class Repository
blob_data_at(sha, '.gitlab-ci.yml')
end
protected
def tree_entry_at(branch_name, path)
branch_exists?(branch_name) &&
# tree_entry is private
raw_repository.send(:tree_entry, commit(branch_name), path)
end
def check_tree_entry_for_dir(branch_name, path)
return unless branch_exists?(branch_name)
entry = tree_entry_at(branch_name, path)
return unless entry
if entry[:type] == :blob
raise Gitlab::Git::Repository::InvalidBlobName.new(
"Directory already exists as a file")
else
raise Gitlab::Git::Repository::InvalidBlobName.new(
"Directory already exists")
end
end
private
def blob_data_at(sha, path)
......@@ -1289,58 +1192,6 @@ class Repository
blob.data
end
def git_action(index, action)
path = normalize_path(action[:file_path])
if action[:action] == :move
previous_path = normalize_path(action[:previous_path])
end
case action[:action]
when :create, :update, :move
mode =
case action[:action]
when :update
index.get(path)[:mode]
when :move
index.get(previous_path)[:mode]
end
mode ||= 0o100644
index.remove(previous_path) if action[:action] == :move
content = if action[:encoding] == 'base64'
Base64.decode64(action[:content])
else
action[:content]
end
detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
unless detect && detect[:type] == :binary
# When writing to the repo directly as we are doing here,
# the `core.autocrlf` config isn't taken into account.
content.gsub!("\r\n", "\n") if self.autocrlf
end
oid = rugged.write(content, :blob)
index.add(path: path, oid: oid, mode: mode)
when :delete
index.remove(path)
end
end
def normalize_path(path)
pathname = Gitlab::Git::PathHelper.normalize_path(path)
if pathname.each_filename.include?('..')
raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
end
pathname.to_s
end
def refs_directory_exists?
return false unless path_with_namespace
......
module Files
class CreateDirService < Files::BaseService
def commit
repository.commit_dir(
repository.create_dir(
current_user,
@file_path,
message: @commit_message,
......
module Files
class CreateService < Files::BaseService
def commit
repository.commit_file(
repository.create_file(
current_user,
@file_path,
@file_content,
message: @commit_message,
branch_name: @target_branch,
update: false,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
......@@ -17,6 +16,10 @@ module Files
def validate
super
if @file_content.nil?
raise_error("You must provide content.")
end
if @file_path =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name ' +
......
module Files
class DestroyService < Files::BaseService
def commit
repository.remove_file(
repository.delete_file(
current_user,
@file_path,
message: @commit_message,
......
......@@ -2,6 +2,8 @@ module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
ACTIONS = %w[create update delete move].freeze
def commit
repository.multi_action(
user: current_user,
......@@ -21,10 +23,19 @@ module Files
super
params[:actions].each_with_index do |action, index|
if ACTIONS.include?(action[:action].to_s)
action[:action] = action[:action].to_sym
else
raise_error("Unknown action type `#{action[:action]}`.")
end
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
......@@ -43,8 +54,6 @@ module Files
validate_delete(action)
when :move
validate_move(action, index)
else
raise_error("Unknown action type `#{action[:action]}`.")
end
end
end
......@@ -92,6 +101,20 @@ module Files
if repository.blob_at_branch(params[:branch], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
raise_error("You must provide content.")
end
end
def validate_update(action)
if action[:content].nil?
raise_error("You must provide content.")
end
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
def validate_delete(action)
......@@ -114,11 +137,5 @@ module Files
params[:actions][index][:content] = blob.data
end
end
def validate_update(action)
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
end
end
......@@ -18,6 +18,10 @@ module Files
def validate
super
if @file_content.nil?
raise_error("You must provide content.")
end
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
end
......
---
title: Adds abitlity to render deploy boards in the frontend side
merge_request: 1233
author:
---
title: 'API: Return 400 for all validation erros in the mebers API'
merge_request: 9523
author: Robert Schilling
......@@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master')
options = {
committer: issue.project.repository.user_to_committer(@user),
author: issue.project.repository.user_to_committer(@user),
commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
file: { content: "content", path: filename, update: false }
}
commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
GitPushService.new(issue.project,
@user,
oldrev: issue.project.repository.commit("master").sha,
......
......@@ -41,9 +41,9 @@ changes are in V4:
- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
#### EE-specific
- Remove the ProjectGitHook API. Use the ProjectPushRule API instead [!1301](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1301)
- Removed `repository_storage` from `PUT /application/settings` and `GET /application/settings` (use `repository_storages` instead) [!1307](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1307)
- Removed `repository_storage` from `PUT /application/settings` and `GET /application/settings` (use `repository_storages` instead) [!1307](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1307)
\ No newline at end of file
......@@ -52,13 +52,6 @@ module API
attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
attrs[:actions].map! do |action|
action[:action] = action[:action].to_sym
action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
action
end
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success
......
......@@ -61,7 +61,6 @@ module API
## EE specific
member = source.members.find_by(user_id: params[:user_id])
conflict!('Member already exists') if member
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
......@@ -69,9 +68,6 @@ module API
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
......@@ -93,9 +89,6 @@ module API
if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
......
......@@ -55,13 +55,6 @@ module API
branch = attrs.delete(:branch_name)
attrs.merge!(branch: branch, start_branch: branch, target_branch: branch)
attrs[:actions].map! do |action|
action[:action] = action[:action].to_sym
action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
action
end
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success
......
......@@ -93,163 +93,6 @@ module Gitlab
commit_id: sha,
)
end
# Commit file in repository and return commit sha
#
# options should contain next structure:
# file: {
# content: 'Lorem ipsum...',
# path: 'documents/story.txt',
# update: true
# },
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Wow such commit',
# branch: 'master',
# update_ref: false
# }
#
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def commit(repository, options, action = :add)
file = options[:file]
update = file[:update].nil? ? true : file[:update]
author = options[:author]
committer = options[:committer]
commit = options[:commit]
repo = repository.rugged
ref = commit[:branch]
update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
parents = []
mode = 0o100644
unless ref.start_with?('refs/')
ref = 'refs/heads/' + ref
end
path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
# Abort if any invalid characters remain (e.g. ../foo)
raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
filename = path_name.to_s
index = repo.index
unless repo.empty?
rugged_ref = repo.references[ref]
raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
last_commit = rugged_ref.target
index.read_tree(last_commit.tree)
parents = [last_commit]
end
if action == :remove
index.remove(filename)
else
file_entry = index.get(filename)
if action == :rename
old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
old_filename = old_path_name.to_s
file_entry = index.get(old_filename)
index.remove(old_filename) unless file_entry.blank?
end
if file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
# Preserve the current file mode if one is available
mode = file_entry[:mode] if file_entry[:mode]
end
content = file[:content]
detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
unless detect && detect[:type] == :binary
# When writing to the repo directly as we are doing here,
# the `core.autocrlf` config isn't taken into account.
content.gsub!("\r\n", "\n") if repository.autocrlf
end
oid = repo.write(content, :blob)
index.add(path: filename, oid: oid, mode: mode)
end
opts = {}
opts[:tree] = index.write_tree(repo)
opts[:author] = author
opts[:committer] = committer
opts[:message] = commit[:message]
opts[:parents] = parents
opts[:update_ref] = ref if update_ref
Rugged::Commit.create(repo, opts)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# Remove file from repository and return commit sha
#
# options should contain next structure:
# file: {
# path: 'documents/story.txt'
# },
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Remove FILENAME',
# branch: 'master'
# }
#
def remove(repository, options)
commit(repository, options, :remove)
end
# Rename file from repository and return commit sha
#
# options should contain next structure:
# file: {
# previous_path: 'documents/old_story.txt'
# path: 'documents/story.txt'
# content: 'Lorem ipsum...',
# update: true
# },
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Rename FILENAME',
# branch: 'master'
# }
#
def rename(repository, options)
commit(repository, options, :rename)
end
end
def initialize(options)
......
module Gitlab
module Git
class Index
DEFAULT_MODE = 0o100644
attr_reader :repository, :raw_index
def initialize(repository)
@repository = repository
@raw_index = repository.rugged.index
end
delegate :read_tree, :get, to: :raw_index
def write_tree
raw_index.write_tree(repository.rugged)
end
def dir_exists?(path)
raw_index.find { |entry| entry[:path].start_with?("#{path}/") }
end
def create(options)
options = normalize_options(options)
file_entry = get(options[:file_path])
if file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists")
end
add_blob(options)
end
def create_dir(options)
options = normalize_options(options)
file_entry = get(options[:file_path])
if file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file")
end
if dir_exists?(options[:file_path])
raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists")
end
options = options.dup
options[:file_path] += '/.gitkeep'
options[:content] = ''
add_blob(options)
end
def update(options)
options = normalize_options(options)
file_entry = get(options[:file_path])
unless file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
end
add_blob(options, mode: file_entry[:mode])
end
def move(options)
options = normalize_options(options)
file_entry = get(options[:previous_path])
unless file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
end
raw_index.remove(options[:previous_path])
add_blob(options, mode: file_entry[:mode])
end
def delete(options)
options = normalize_options(options)
file_entry = get(options[:file_path])
unless file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
end
raw_index.remove(options[:file_path])
end
private
def normalize_options(options)
options = options.dup
options[:file_path] = normalize_path(options[:file_path]) if options[:file_path]
options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path]
options
end
def normalize_path(path)
pathname = Gitlab::Git::PathHelper.normalize_path(path.dup)
if pathname.each_filename.include?('..')
raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
end
pathname.to_s
end
def add_blob(options, mode: nil)
content = options[:content]
content = Base64.decode64(content) if options[:encoding] == 'base64'
detect = CharlockHolmes::EncodingDetector.new.detect(content)
unless detect && detect[:type] == :binary
# When writing to the repo directly as we are doing here,
# the `core.autocrlf` config isn't taken into account.
content.gsub!("\r\n", "\n") if repository.autocrlf
end
oid = repository.rugged.write(content, :blob)
raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
rescue Rugged::IndexError => e
raise Gitlab::Git::Repository::InvalidBlobName.new(e.message)
end
end
end
end
......@@ -837,57 +837,6 @@ module Gitlab
rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
end
# Create a new directory with a .gitkeep file. Creates
# all required nested directories (i.e. mkdir -p behavior)
#
# options should contain next structure:
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Wow such commit',
# branch: 'master',
# update_ref: false
# }
def mkdir(path, options = {})
# Check if this directory exists; if it does, then don't bother
# adding .gitkeep file.
ref = options[:commit][:branch]
path = Gitlab::Git::PathHelper.normalize_path(path).to_s
rugged_ref = rugged.ref(ref)
raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
target_commit = rugged_ref.target
raise InvalidRef.new("Invalid target commit") if target_commit.nil?
entry = tree_entry(target_commit, path)
if entry
if entry[:type] == :blob
raise InvalidBlobName.new("Directory already exists as a file")
else
raise InvalidBlobName.new("Directory already exists")
end
end
options[:file] = {
content: '',
path: "#{path}/.gitkeep",
update: true
}
Gitlab::Git::Blob.commit(self, options)
end
# Returns result like "git ls-files" , recursive and full file path
#
# Ex.
......
......@@ -14,8 +14,8 @@ describe Projects::TemplatesController do
before do
project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, 'something valid',
message: 'test 3', branch_name: 'master', update: false)
project.repository.create_file(user, file_path_1, 'something valid',
message: 'test 3', branch_name: 'master')
end
describe '#show' do
......
......@@ -171,27 +171,24 @@ FactoryGirl.define do
project.add_user(args[:user], args[:access])
project.repository.commit_file(
project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/bug.md",
'something valid',
message: 'test 3',
branch_name: 'master',
update: false)
project.repository.commit_file(
branch_name: 'master')
project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/template_test.md",
'template_test',
message: 'test 1',
branch_name: 'master',
update: false)
project.repository.commit_file(
branch_name: 'master')
project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/feature_proposal.md",
'feature_proposal',
message: 'test 2',
branch_name: 'master',
update: false)
branch_name: 'master')
end
end
end
......
......@@ -6,7 +6,7 @@ feature 'project owner creates a license file', feature: true, js: true do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
project.repository.remove_file(project_master, 'LICENSE',
project.repository.delete_file(project_master, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
project.team << [project_master, :master]
login_as(project_master)
......
......@@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do
let(:description_addition) { ' appending to description' }
background do
project.repository.commit_file(
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
template_content,
message: 'added issue template',
branch_name: 'master',
update: false)
project.repository.commit_file(
branch_name: 'master')
project.repository.create_file(
user,
'.gitlab/issue_templates/test.md',
longtemplate_content,
message: 'added issue template',
branch_name: 'master',
update: false)
branch_name: 'master')
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
......@@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
project.repository.commit_file(
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
template_content,
message: 'added issue template',
branch_name: 'master',
update: false)
branch_name: 'master')
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
fill_in :'issue[description]', with: prior_description
......@@ -104,13 +101,12 @@ feature 'issuable templates', feature: true, js: true do
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
background do
project.repository.commit_file(
project.repository.create_file(
user,
'.gitlab/merge_request_templates/feature-proposal.md',
template_content,
message: 'added merge request template',
branch_name: 'master',
update: false)
branch_name: 'master')
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
......@@ -135,13 +131,12 @@ feature 'issuable templates', feature: true, js: true do
fork_project.team << [fork_user, :master]
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
login_as fork_user
project.repository.commit_file(
project.repository.create_file(
fork_user,
'.gitlab/merge_request_templates/feature-proposal.md',
template_content,
message: 'added merge request template',
branch_name: 'master',
update: false)
branch_name: 'master')
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
......
const Vue = require('vue');
const DeployBoard = require('~/environments/components/deploy_board_component');
const Service = require('~/environments/services/environments_service');
const { deployBoardMockData } = require('./mock_data');
describe('Deploy Board', () => {
let DeployBoardComponent;
beforeEach(() => {
DeployBoardComponent = Vue.extend(DeployBoard);
});
describe('successfull request', () => {
const deployBoardInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(deployBoardMockData), {
status: 200,
}));
};
let component;
beforeEach(() => {
Vue.http.interceptors.push(deployBoardInterceptor);
this.service = new Service('environments');
component = new DeployBoardComponent({
propsData: {
store: {},
service: this.service,
deployBoardData: deployBoardMockData,
environmentID: 1,
},
}).$mount();
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, deployBoardInterceptor,
);
});
it('should render percentage with completion value provided', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-information .percentage').textContent,
).toEqual(`${deployBoardMockData.completion}%`);
done();
}, 0);
});
it('should render all instances', (done) => {
setTimeout(() => {
const instances = component.$el.querySelectorAll('.deploy-board-instances-container div');
expect(instances.length).toEqual(deployBoardMockData.instances.length);
expect(
instances[2].classList.contains(`deploy-board-instance-${deployBoardMockData.instances[2].status}`),
).toBe(true);
done();
}, 0);
});
it('should render an abort and a rollback button with the provided url', (done) => {
setTimeout(() => {
const buttons = component.$el.querySelectorAll('.deploy-board-actions a');
expect(buttons[0].getAttribute('href')).toEqual(deployBoardMockData.rollback_url);
expect(buttons[1].getAttribute('href')).toEqual(deployBoardMockData.abort_url);
done();
}, 0);
});
});
describe('unsuccessfull request', () => {
const deployBoardErrorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
let component;
beforeEach(() => {
Vue.http.interceptors.push(deployBoardErrorInterceptor);
this.service = new Service('environments');
component = new DeployBoardComponent({
propsData: {
store: {},
service: this.service,
deployBoardData: {},
environmentID: 1,
},
}).$mount();
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, deployBoardErrorInterceptor);
});
it('should render empty state', (done) => {
setTimeout(() => {
expect(component.$el.children.length).toEqual(0);
done();
}, 0);
});
});
});
const Vue = require('vue');
const DeployBoardInstance = require('~/environments/components/deploy_board_instance_component');
describe('Deploy Board Instance', () => {
let DeployBoardInstanceComponent;
beforeEach(() => {
DeployBoardInstanceComponent = Vue.extend(DeployBoardInstance);
});
it('should render a div with the correct css status and tooltip data', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'ready',
tooltipText: 'This is a pod',
},
}).$mount();
expect(component.$el.classList.contains('deploy-board-instance-ready')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('This is a pod');
});
it('should render a div without tooltip data', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
},
}).$mount();
expect(component.$el.classList.contains('deploy-board-instance-deploying')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('');
});
});
......@@ -26,6 +26,9 @@ describe('Environment item', () => {
model: mockItem,
canCreateDeployment: false,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
});
......@@ -114,6 +117,9 @@ describe('Environment item', () => {
model: environment,
canCreateDeployment: true,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
});
......
......@@ -49,7 +49,7 @@ describe('Environment', () => {
});
});
describe('with paginated environments', () => {
describe('with environments', () => {
const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
environments: [environment],
......@@ -142,6 +142,17 @@ describe('Environment', () => {
}, 0);
});
});
describe('deploy boards', () => {
it('should render arrow to open deploy boards', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
done();
}, 0);
});
});
});
});
......
......@@ -9,22 +9,101 @@ describe('Environment item', () => {
it('Should render a table', () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
latest: {
environment_path: 'url',
},
environment_path: 'url',
};
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [{ mockItem }],
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
expect(component.$el.tagName).toEqual('TABLE');
});
it('should render deploy board container when data is provided', () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
rollout_status_path: 'url',
hasDeployBoard: true,
deployBoardData: {
instances: [
{ status: 'ready', tooltip: 'foo' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
},
isDeployBoardVisible: true,
};
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem],
canCreateDeployment: true,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
expect(component.$el.querySelector('.js-deploy-board-row')).toBeDefined();
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
});
it('should toggle deploy board visibility when arrow is clicked', () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
rollout_status_path: 'url',
hasDeployBoard: true,
deployBoardData: {
instances: [
{ status: 'ready', tooltip: 'foo' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
},
isDeployBoardVisible: false,
};
const spy = jasmine.createSpy('spy');
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem],
canCreateDeployment: true,
canReadEnvironment: true,
toggleDeployBoard: spy,
store: {},
service: {},
},
});
component.$el.querySelector('.deploy-board-icon').click();
expect(spy).toHaveBeenCalled();
});
});
const Store = require('~/environments/stores/environments_store');
const { environmentsList, serverData } = require('./mock_data');
const { serverData, deployBoardMockData } = require('./mock_data');
(() => {
describe('Store', () => {
describe('Environments Store', () => {
let store;
beforeEach(() => {
......@@ -16,10 +16,53 @@ const { environmentsList, serverData } = require('./mock_data');
expect(store.state.paginationInformation).toEqual({});
});
it('should store environments', () => {
store.storeEnvironments(serverData);
expect(store.state.environments.length).toEqual(serverData.length);
expect(store.state.environments[0]).toEqual(environmentsList[0]);
describe('store environments', () => {
it('should store environments', () => {
store.storeEnvironments(serverData);
expect(store.state.environments.length).toEqual(serverData.length);
});
it('should store a non folder environment with deploy board if rollout_status_path key is provided', () => {
const environment = {
name: 'foo',
size: 1,
id: 1,
rollout_status_path: 'url',
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
expect(store.state.environments[0].deployBoardData).toEqual({});
});
it('should add folder keys when environment is a folder', () => {
const environment = {
name: 'bar',
size: 3,
id: 2,
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].isFolder).toEqual(true);
expect(store.state.environments[0].folderName).toEqual('bar');
});
it('should extract content of `latest` key when provided', () => {
const environment = {
name: 'bar',
size: 3,
id: 2,
latest: {
last_deployment: {},
isStoppable: true,
},
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].last_deployment).toEqual({});
expect(store.state.environments[0].isStoppable).toEqual(true);
});
});
it('should store available count', () => {
......@@ -32,27 +75,51 @@ const { environmentsList, serverData } = require('./mock_data');
expect(store.state.stoppedCounter).toEqual(2);
});
it('should store pagination information', () => {
const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '2',
'X-TOTAL': '37',
'X-Total-Pages': '2',
};
const expectedResult = {
perPage: 1,
page: 1,
total: 37,
totalPages: 2,
nextPage: 2,
previousPage: 2,
};
store.setPagination(pagination);
expect(store.state.paginationInformation).toEqual(expectedResult);
describe('store pagination', () => {
it('should store normalized and integer pagination information', () => {
const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '2',
'X-TOTAL': '37',
'X-Total-Pages': '2',
};
const expectedResult = {
perPage: 1,
page: 1,
total: 37,
totalPages: 2,
nextPage: 2,
previousPage: 2,
};
store.setPagination(pagination);
expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
id: 1,
};
store.storeEnvironments([environment]);
});
it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
});
it('should store deploy board data for given environment id', () => {
store.storeDeployBoard(1, deployBoardMockData);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
});
});
})();
......@@ -141,6 +141,17 @@ describe('Environments Folder View', () => {
}, 0);
});
});
describe('deploy boards', () => {
it('should render arrow to open deploy boards', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
done();
}, 0);
});
});
});
describe('unsuccessfull request', () => {
......
......@@ -12,6 +12,7 @@ const environmentsList = [
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path',
},
{
folderName: 'build',
......@@ -82,11 +83,49 @@ const environment = {
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path',
},
};
const deployBoardMockData = {
instances: [
{ status: 'finished', tooltip: 'tanuki-2334 Finished' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished' },
{ status: 'finished', tooltip: 'tanuki-2334 Finished' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished' },
{ status: 'deploying', tooltip: 'tanuki-2341 Deploying' },
{ status: 'deploying', tooltip: 'tanuki-2342 Deploying' },
{ status: 'deploying', tooltip: 'tanuki-2343 Deploying' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed' },
{ status: 'ready', tooltip: 'tanuki-2345 Ready' },
{ status: 'ready', tooltip: 'tanuki-2346 Ready' },
{ status: 'preparing', tooltip: 'tanuki-2348 Preparing' },
{ status: 'preparing', tooltip: 'tanuki-2349 Preparing' },
{ status: 'preparing', tooltip: 'tanuki-2350 Preparing' },
{ status: 'preparing', tooltip: 'tanuki-2353 Preparing' },
{ status: 'waiting', tooltip: 'tanuki-2354 Waiting' },
{ status: 'waiting', tooltip: 'tanuki-2355 Waiting' },
{ status: 'waiting', tooltip: 'tanuki-2356 Waiting' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
};
module.exports = {
environmentsList,
environment,
serverData,
deployBoardMockData,
};
......@@ -193,7 +193,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
white_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev
new_rev = project.repository.commit_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master", update: false)
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev)
......@@ -216,7 +216,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
black_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev
new_rev = project.repository.commit_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master", update: false)
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev)
......
......@@ -557,13 +557,12 @@ describe Gitlab::Elastic::SearchResults, lib: true do
context 'Commits' do
it 'finds right set of commits' do
[internal_project, private_project1, private_project2, public_project].each do |project|
project.repository.commit_file(
project.repository.create_file(
user,
'test-file',
'search test',
message: 'search test',
branch_name: 'master',
update: false
branch_name: 'master'
)
project.repository.index_commits
......@@ -590,13 +589,12 @@ describe Gitlab::Elastic::SearchResults, lib: true do
context 'Blobs' do
it 'finds right set of blobs' do
[internal_project, private_project1, private_project2, public_project].each do |project|
project.repository.commit_file(
project.repository.create_file(
user,
'test-file',
'tesla',
message: 'search test',
branch_name: 'master',
update: false
branch_name: 'master'
)
project.repository.index_blobs
......
......@@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
describe :commit do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:commit_options) do
{
file: {
content: 'Lorem ipsum...',
path: 'documents/story.txt'
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Wow such commit',
branch: 'fix-mode'
}
}
end
let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
let(:commit) { repository.lookup(commit_sha) }
it 'should add file with commit' do
# Commit message valid
expect(commit.message).to eq('Wow such commit')
tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
# Directory was created
expect(tree[:type]).to eq(:tree)
# File was created
expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
end
describe "ref updating" do
it 'creates a commit but does not udate a ref' do
commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
commit = repository.lookup(commit_sha)
# Commit message valid
expect(commit.message).to eq('Wow such commit')
# Does not update any related ref
expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
end
end
describe 'reject updates' do
it 'should reject updates' do
commit_options[:file][:update] = false
commit_options[:file][:path] = 'files/executables/ls'
expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
end
end
describe 'file modes' do
it 'should preserve file modes with commit' do
commit_options[:file][:path] = 'files/executables/ls'
entry = Gitlab::Git::Blob.find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
expect(entry[:filemode]).to eq(0100755)
end
end
end
describe :rename do
let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
let(:rename_options) do
{
file: {
path: 'NEWCONTRIBUTING.md',
previous_path: 'CONTRIBUTING.md',
content: 'Lorem ipsum...',
update: true
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Rename readme',
branch: 'master'
}
}
end
let(:rename_options2) do
{
file: {
content: 'Lorem ipsum...',
path: 'bin/new_executable',
previous_path: 'bin/executable',
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Updates toberenamed.txt',
branch: 'master',
update_ref: false
}
}
end
it 'maintains file permissions when renaming' do
mode = 0o100755
Gitlab::Git::Blob.rename(repository, rename_options2)
expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
end
it 'renames the file with commit and not change file permissions' do
ref = rename_options[:commit][:branch]
expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
end
end
describe :remove do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:commit_options) do
{
file: {
path: 'README.md'
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Remove readme',
branch: 'feature'
}
}
end
let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
let(:commit) { repository.lookup(commit_sha) }
let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
it 'should remove file with commit' do
# Commit message valid
expect(commit.message).to eq('Remove readme')
# File was removed
expect(blob).to be_nil
end
end
describe :lfs_pointers do
context 'file a valid lfs pointer' do
let(:blob) do
......
require 'spec_helper'
describe Gitlab::Git::Index, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:index) { described_class.new(repository) }
before do
index.read_tree(repository.lookup('master').tree)
end
describe '#create' do
let(:options) do
{
content: 'Lorem ipsum...',
file_path: 'documents/story.txt'
}
end
context 'when no file at that path exists' do
it 'creates the file in the index' do
index.create(options)
entry = index.get(options[:file_path])
expect(entry).not_to be_nil
expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
end
end
context 'when a file at that path exists' do
before do
options[:file_path] = 'files/executables/ls'
end
it 'raises an error' do
expect { index.create(options) }.to raise_error('Filename already exists')
end
end
context 'when content is in base64' do
before do
options[:content] = Base64.encode64(options[:content])
options[:encoding] = 'base64'
end
it 'decodes base64' do
index.create(options)
entry = index.get(options[:file_path])
expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content]))
end
end
context 'when content contains CRLF' do
before do
repository.autocrlf = :input
options[:content] = "Hello,\r\nWorld"
end
it 'converts to LF' do
index.create(options)
entry = index.get(options[:file_path])
expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld")
end
end
end
describe '#create_dir' do
let(:options) do
{
file_path: 'newdir'
}
end
context 'when no file or dir at that path exists' do
it 'creates the dir in the index' do
index.create_dir(options)
entry = index.get(options[:file_path] + '/.gitkeep')
expect(entry).not_to be_nil
end
end
context 'when a file at that path exists' do
before do
options[:file_path] = 'files/executables/ls'
end
it 'raises an error' do
expect { index.create_dir(options) }.to raise_error('Directory already exists as a file')
end
end
context 'when a directory at that path exists' do
before do
options[:file_path] = 'files/executables'
end
it 'raises an error' do
expect { index.create_dir(options) }.to raise_error('Directory already exists')
end
end
end
describe '#update' do
let(:options) do
{
content: 'Lorem ipsum...',
file_path: 'README.md'
}
end
context 'when no file at that path exists' do
before do
options[:file_path] = 'documents/story.txt'
end
it 'raises an error' do
expect { index.update(options) }.to raise_error("File doesn't exist")
end
end
context 'when a file at that path exists' do
it 'updates the file in the index' do
index.update(options)
entry = index.get(options[:file_path])
expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
end
it 'preserves file mode' do
options[:file_path] = 'files/executables/ls'
index.update(options)
entry = index.get(options[:file_path])
expect(entry[:mode]).to eq(0100755)
end
end
end
describe '#move' do
let(:options) do
{
content: 'Lorem ipsum...',
previous_path: 'README.md',
file_path: 'NEWREADME.md'
}
end
context 'when no file at that path exists' do
it 'raises an error' do
options[:previous_path] = 'documents/story.txt'
expect { index.move(options) }.to raise_error("File doesn't exist")
end
end
context 'when a file at that path exists' do
it 'removes the old file in the index' do
index.move(options)
entry = index.get(options[:previous_path])
expect(entry).to be_nil
end
it 'creates the new file in the index' do
index.move(options)
entry = index.get(options[:file_path])
expect(entry).not_to be_nil
expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
end
it 'preserves file mode' do
options[:previous_path] = 'files/executables/ls'
index.move(options)
entry = index.get(options[:file_path])
expect(entry[:mode]).to eq(0100755)
end
end
end
describe '#delete' do
let(:options) do
{
file_path: 'README.md'
}
end
context 'when no file at that path exists' do
before do
options[:file_path] = 'documents/story.txt'
end
it 'raises an error' do
expect { index.delete(options) }.to raise_error("File doesn't exist")
end
end
context 'when a file at that path exists' do
it 'removes the file in the index' do
index.delete(options)
entry = index.get(options[:file_path])
expect(entry).to be_nil
end
end
end
end
......@@ -844,81 +844,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#mkdir' do
let(:commit_options) do
{
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Test message',
branch: 'refs/heads/fix',
}
}
end
def generate_diff_for_path(path)
"diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/#{path}/.gitkeep\n"
end
shared_examples 'mkdir diff check' do |path, expected_path|
it 'creates a directory' do
result = repository.mkdir(path, commit_options)
expect(result).not_to eq(nil)
# Verify another mkdir doesn't create a directory that already exists
expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists')
end
end
describe 'creates a directory in root directory' do
it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir'
end
describe 'creates a directory in subdirectory' do
it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test'
end
describe 'creates a directory in subdirectory with a slash' do
it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2'
end
describe 'creates a directory in subdirectory with multiple slashes' do
it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3'
end
describe 'handles relative paths' do
it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative'
end
describe 'creates nested directories' do
it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test'
end
it 'does not attempt to create a directory with invalid relative path' do
expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path')
end
it 'does not attempt to overwrite a file' do
expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file')
end
it 'does not attempt to overwrite a directory' do
expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists')
end
end
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
......
......@@ -222,16 +222,21 @@ describe Gitlab::GitAccess, lib: true do
def merge_into_protected_branch
@protected_branch_merge_commit ||= begin
stub_git_hooks
project.repository.add_branch(user, unprotected_branch, 'feature')
target_branch = project.repository.lookup('feature')
source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, message: FFaker::HipsterIpsum.sentence, branch_name: unprotected_branch, update: false)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
merge_index = rugged.merge_commits(target_branch, source_branch)
Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
end
stub_git_hooks
project.repository.add_branch(user, unprotected_branch, 'feature')
target_branch = project.repository.lookup('feature')
source_branch = project.repository.create_file(
user,
FFaker::InternetSE.login_user_name,
FFaker::HipsterIpsum.paragraph,
message: FFaker::HipsterIpsum.sentence,
branch_name: unprotected_branch)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
merge_index = rugged.merge_commits(target_branch, source_branch)
Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
end
end
# Run permission checks for a user
......
......@@ -21,13 +21,12 @@ describe 'CycleAnalytics#production', feature: true do
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
sha = context.project.repository.commit_file(
sha = context.project.repository.create_file(
context.user,
context.random_git_name,
'content',
message: 'commit message',
branch_name: 'master',
update: false)
branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
......
......@@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', feature: true do
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
sha = context.project.repository.commit_file(
sha = context.project.repository.create_file(
context.user,
context.random_git_name,
'content',
message: 'commit message',
branch_name: 'master',
update: false)
branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
......
......@@ -2156,7 +2156,7 @@ describe Project, models: true do
end
before do
project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master')
end
context 'when there is a .gitlab/route-map.yml at the commit' do
......
This diff is collapsed.
......@@ -146,16 +146,6 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
context 'with project path in URL' do
let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
it 'a new file in project repo' do
post api(url, user), valid_c_params
expect(response).to have_http_status(201)
end
end
end
context :delete do
......
......@@ -127,7 +127,7 @@ describe API::Files, api: true do
end
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:commit_file).
allow_any_instance_of(Repository).to receive(:create_file).
and_return(false)
post api("/projects/#{project.id}/repository/files", user), valid_params
......@@ -215,7 +215,7 @@ describe API::Files, api: true do
end
it "returns a 400 if fails to create file" do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
......
......@@ -173,11 +173,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
it 'returns 422 when access_level is not valid' do
it 'returns 400 when access_level is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234
expect(response).to have_http_status(422)
expect(response).to have_http_status(400)
end
end
end
......@@ -247,11 +247,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
it 'returns 422 when access level is not valid' do
it 'returns 400 when access level is not valid' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234
expect(response).to have_http_status(422)
expect(response).to have_http_status(400)
end
end
end
......@@ -363,7 +363,7 @@ describe API::Members, api: true do
post api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER
expect(response).to have_http_status(422)
expect(response).to have_http_status(400)
end.to change { project.members.count }.by(0)
end
end
......
......@@ -127,7 +127,7 @@ describe API::V3::Files, api: true do
end
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:commit_file).
allow_any_instance_of(Repository).to receive(:create_file).
and_return(false)
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
......@@ -215,7 +215,7 @@ describe API::V3::Files, api: true do
end
it "returns a 400 if fails to create file" do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
......
......@@ -66,13 +66,12 @@ describe MergeRequests::ResolveService do
context 'when the source project is a fork and does not contain the HEAD of the target branch' do
let!(:target_head) do
project.repository.commit_file(
project.repository.create_file(
user,
'new-file-in-target',
'',
message: 'Add new file in target',
branch_name: 'conflict-start',
update: false)
branch_name: 'conflict-start')
end
before do
......
......@@ -9,14 +9,7 @@ module CycleAnalyticsHelpers
commit_shas = Array.new(count) do |index|
filename = random_git_name
options = {
committer: project.repository.user_to_committer(user),
author: project.repository.user_to_committer(user),
commit: { message: message, branch: branch_name, update_ref: true },
file: { content: "content", path: filename, update: false }
}
commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name)
project.repository.commit(commit_sha)
commit_sha
......@@ -35,13 +28,12 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
sha = project.repository.commit_file(
sha = project.repository.create_file(
user,
random_git_name,
'content',
message: 'commit message',
branch_name: source_branch,
update: false)
branch_name: source_branch)
project.repository.commit(sha)
opts = {
......
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