Commit 554426dd authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 24196-protected-variables

* upstream/master: (141 commits)
  Add Documentation for GIT_CHECKOUT variable
  Ui improvements for count badges and permission badges
  Rename the other jobs
  Update jobs_spec for changes from builds_spec
  Update docs related to protected actions
  Add changelog for protected branches abilities fix
  Ask for an example project for bug reports
  Center loading spinner in issuable filters
  Fix chat commands specs related to protected actions
  Fix builds controller specs related to protected actions
  Fix pipeline retry specs related to protected actions
  Fix environment model specs related to protected actions
  Fix build factory specs related to protected actions
  Fix job play service specs related to protected actions
  Fix play status specs related to protected actions
  Fix deploy chat command specs for protected actions
  Fix environment specs related to protected actions
  Use another scope to add the - prefix, feedback:
  Fix pipeline processing specs related to protected actions
  Fix build entity specs related to protected actions
  ...
parents 8a1a73a7 f06daa26
...@@ -20,6 +20,12 @@ Please remove this notice if you're confident your issue isn't a duplicate. ...@@ -20,6 +20,12 @@ Please remove this notice if you're confident your issue isn't a duplicate.
(How one can reproduce the issue - this is very important) (How one can reproduce the issue - this is very important)
### Example Project
(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
### What is the current *bug* behavior? ### What is the current *bug* behavior?
(What actually happens) (What actually happens)
......
...@@ -109,7 +109,7 @@ gem 'seed-fu', '~> 2.3.5' ...@@ -109,7 +109,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.5.1' gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
......
...@@ -141,10 +141,8 @@ GEM ...@@ -141,10 +141,8 @@ GEM
database_cleaner (1.5.3) database_cleaner (1.5.3)
debug_inspector (0.0.2) debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8) debugger-ruby_core_source (1.3.8)
deckar01-task_list (1.0.6) deckar01-task_list (2.0.0)
activesupport (~> 4.0)
html-pipeline html-pipeline
rack (~> 1.0)
default_value_for (3.0.2) default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1) activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
...@@ -895,7 +893,7 @@ DEPENDENCIES ...@@ -895,7 +893,7 @@ DEPENDENCIES
creole (~> 0.5.0) creole (~> 0.5.0)
d3_rails (~> 3.5.0) d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 1.0.6) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
......
...@@ -118,7 +118,7 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -118,7 +118,7 @@ import ShortcutsBlob from './shortcuts_blob';
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:builds:show': case 'projects:jobs:show':
new Build(); new Build();
break; break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
......
...@@ -194,6 +194,7 @@ window.DropzoneInput = (function() { ...@@ -194,6 +194,7 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection); $(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`; textarea.style.height = `${textarea.scrollHeight}px`;
formTextarea.get(0).dispatchEvent(new Event('input'));
return formTextarea.trigger('input'); return formTextarea.trigger('input');
}; };
......
<script> <script>
/* global Flash */
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index'; import Service from '../services/index';
import Store from '../stores'; import Store from '../stores';
import titleComponent from './title.vue'; import titleComponent from './title.vue';
import descriptionComponent from './description.vue'; import descriptionComponent from './description.vue';
import formComponent from './form.vue';
import '../../lib/utils/url_utility';
export default { export default {
props: { props: {
...@@ -12,15 +16,27 @@ export default { ...@@ -12,15 +16,27 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
canMove: {
required: true,
type: Boolean,
},
canUpdate: { canUpdate: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
canDestroy: {
required: true,
type: Boolean,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
}, },
initialTitle: { initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -34,10 +50,40 @@ export default { ...@@ -34,10 +50,40 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
isConfidential: {
type: Boolean,
required: true,
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
titleHtml: this.initialTitle, titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml, descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText, descriptionText: this.initialDescriptionText,
}); });
...@@ -45,19 +91,97 @@ export default { ...@@ -45,19 +91,97 @@ export default {
return { return {
store, store,
state: store.state, state: store.state,
showForm: false,
}; };
}, },
computed: {
formState() {
return this.store.formState;
},
},
components: { components: {
descriptionComponent, descriptionComponent,
titleComponent, titleComponent,
formComponent,
},
methods: {
openForm() {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
confidential: this.isConfidential,
description: this.state.descriptionText,
lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false,
});
}
},
closeForm() {
this.showForm = false;
},
updateIssuable() {
const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
if (!canPostUpdate) {
this.store.setFormState({
updateLoading: false,
});
return;
}
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
} else if (data.confidential !== this.isConfidential) {
gl.utils.visitUrl(location.pathname);
}
return this.service.getData();
})
.then(res => res.json())
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error updating issue');
});
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then((data) => {
// Stop the poll so we don't get 404's with the issue not existing
this.poll.stop();
gl.utils.visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error deleting issue');
});
},
}, },
created() { created() {
const resource = new Service(this.endpoint); this.service = new Service(this.endpoint);
const poll = new Poll({ this.poll = new Poll({
resource, resource: this.service,
method: 'getData', method: 'getData',
successCallback: (res) => { successCallback: (res) => {
this.store.updateState(res.json()); const data = res.json();
const shouldUpdate = this.store.stateShouldUpdate(data);
this.store.updateState(data);
if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
this.store.formState.lockedWarningVisible = true;
}
}, },
errorCallback(err) { errorCallback(err) {
throw new Error(err); throw new Error(err);
...@@ -65,22 +189,46 @@ export default { ...@@ -65,22 +189,46 @@ export default {
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
poll.makeRequest(); this.poll.makeRequest();
} }
Visibility.change(() => { Visibility.change(() => {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
poll.restart(); this.poll.restart();
} else { } else {
poll.stop(); this.poll.stop();
} }
}); });
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<form-component
v-if="canUpdate && showForm"
:form-state="formState"
:can-move="canMove"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
:markdown-docs="markdownDocs"
:markdown-preview-url="markdownPreviewUrl"
:project-path="projectPath"
:project-namespace="projectNamespace"
:projects-autocomplete-url="projectsAutocompleteUrl"
/>
<div v-else>
<title-component <title-component
:issuable-ref="issuableRef" :issuable-ref="issuableRef"
:title-html="state.titleHtml" :title-html="state.titleHtml"
...@@ -93,4 +241,5 @@ export default { ...@@ -93,4 +241,5 @@ export default {
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:task-status="state.taskStatus" /> :task-status="state.taskStatus" />
</div> </div>
</div>
</template> </template>
...@@ -18,11 +18,13 @@ ...@@ -18,11 +18,13 @@
}, },
updatedAt: { updatedAt: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
taskStatus: { taskStatus: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { data() {
...@@ -83,6 +85,7 @@ ...@@ -83,6 +85,7 @@
<template> <template>
<div <div
v-if="descriptionHtml"
class="description" class="description"
:class="{ :class="{
'js-task-list-container': canUpdate 'js-task-list-container': canUpdate
......
<script>
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
export default {
mixins: [updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
},
data() {
return {
deleteLoading: false,
};
},
computed: {
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
},
methods: {
closeForm() {
eventHub.$emit('close.form');
},
deleteIssuable() {
// eslint-disable-next-line no-alert
if (confirm('Issue will be removed! Are you sure?')) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
}
},
},
};
</script>
<template>
<div class="prepend-top-default append-bottom-default clearfix">
<button
class="btn btn-save pull-left"
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
@click.prevent="updateIssuable">
Save changes
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="formState.updateLoading">
</i>
</button>
<button
class="btn btn-default pull-right"
type="button"
@click="closeForm">
Cancel
</button>
<button
v-if="canDestroy"
class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
:disabled="deleteLoading"
@click="deleteIssuable">
Delete
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="deleteLoading">
</i>
</button>
</div>
</template>
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset class="checkbox">
<label for="issue-confidential">
<input
type="checkbox"
value="1"
id="issue-confidential"
v-model="formState.confidential" />
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</template>
<script>
/* global Flash */
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
},
components: {
markdownField,
},
mounted() {
this.$refs.textarea.focus();
},
};
</script>
<template>
<div class="common-note-form">
<label
class="sr-only"
for="issue-description">
Description
</label>
<markdown-field
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-slash-commands="false"
aria-label="Description"
v-model="formState.description"
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@keydown.meta.enter="updateIssuable">
</textarea>
</markdown-field>
</div>
</template>
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
},
computed: {
issuableTemplatesJson() {
return JSON.stringify(this.issuableTemplates);
},
},
mounted() {
// Create the editor for the template
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
editor.setValue = (val) => {
this.formState.description = val;
};
editor.getValue = () => this.formState.description;
this.issuableTemplate = new gl.IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
},
};
</script>
<template>
<div
class="dropdown js-issuable-selector-wrap"
data-issuable-type="issue">
<button
class="dropdown-menu-toggle js-issuable-selector"
type="button"
ref="toggle"
data-field-name="issuable_template"
data-selected="null"
data-toggle="dropdown"
:data-namespace-path="projectNamespace"
:data-project-path="projectPath"
:data-data="issuableTemplatesJson">
<span class="dropdown-toggle-text">
Choose a template
</span>
<i
aria-hidden="true"
class="fa fa-chevron-down">
</i>
</button>
<div class="dropdown-menu dropdown-select">
<div class="dropdown-title">
Choose a template
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i
aria-hidden="true"
class="fa fa-times dropdown-menu-close-icon">
</i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
placeholder="Filter"
autocomplete="off" />
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search">
</i>
<i
role="button"
aria-label="Clear templates search input"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<a class="no-template">
No template
</a>
</li>
<li>
<a class="reset-template">
Reset template
</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
formState: {
type: Object,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
mounted() {
const $moveDropdown = $(this.$refs['move-dropdown']);
$moveDropdown.select2({
ajax: {
url: this.projectsAutocompleteUrl,
quietMillis: 125,
data(term, page, context) {
return {
search: term,
offset_id: context,
};
},
results(data) {
const more = data.length >= 50;
const context = data[data.length - 1] ? data[data.length - 1].id : null;
return {
results: data,
more,
context,
};
},
},
formatResult(project) {
return project.name_with_namespace;
},
formatSelection(project) {
return project.name_with_namespace;
},
})
.on('change', (e) => {
this.formState.move_to_project_id = parseInt(e.target.value, 10);
});
},
beforeDestroy() {
$(this.$refs['move-dropdown']).select2('destroy');
},
};
</script>
<template>
<fieldset>
<label
for="issuable-move"
class="sr-only">
Move
</label>
<div class="issuable-form-select-holder append-right-5">
<input
ref="move-dropdown"
type="hidden"
id="issuable-move"
data-placeholder="Move to a different project" />
</div>
<span
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
<i
class="fa fa-question-circle"
aria-hidden="true">
</i>
</span>
</fieldset>
</template>
<script>
import updateMixin from '../../mixins/update';
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset>
<label
class="sr-only"
for="issue-title">
Title
</label>
<input
id="issue-title"
class="form-control"
type="text"
placeholder="Issue title"
aria-label="Issue title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable" />
</fieldset>
</template>
<script>
import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
import projectMove from './fields/project_move.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default {
props: {
canMove: {
type: Boolean,
required: true,
},
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
projectMove,
confidentialCheckbox,
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
},
},
};
</script>
<template>
<form>
<locked-warning v-if="formState.lockedWarningVisible" />
<div class="row">
<div
class="col-sm-4 col-lg-3"
v-if="hasIssuableTemplates">
<description-template
:form-state="formState"
:issuable-templates="issuableTemplates"
:project-path="projectPath"
:project-namespace="projectNamespace" />
</div>
<div
:class="{
'col-sm-8 col-lg-9': hasIssuableTemplates,
'col-xs-12': !hasIssuableTemplates,
}">
<title-field
:form-state="formState"
:issuable-templates="issuableTemplates" />
</div>
</div>
<description-field
:form-state="formState"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
<confidential-checkbox
:form-state="formState" />
<project-move
v-if="canMove"
:form-state="formState"
:projects-autocomplete-url="projectsAutocompleteUrl" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
</form>
</template>
<script>
export default {
computed: {
currentPath() {
return location.pathname;
},
},
};
</script>
<template>
<div class="alert alert-danger">
Someone edited the issue at the same time you did. Please check out
<a
:href="currentPath"
target="_blank"
rel="nofollow">the issue</a>
and make sure your changes will not unintentionally remove theirs.
</div>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue'; import Vue from 'vue';
import eventHub from './event_hub';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.issuable-edit').on('click', (e) => {
e.preventDefault();
eventHub.$emit('open.form');
});
return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
components: { components: {
issuableApp, issuableApp,
}, },
data() { data() {
const issuableElement = this.$options.el;
const issuableTitleElement = issuableElement.querySelector('.title');
const issuableDescriptionElement = issuableElement.querySelector('.wiki');
const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
const {
canUpdate,
endpoint,
issuableRef,
} = issuableElement.dataset;
return { return {
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), ...initialData,
endpoint,
issuableRef,
initialTitle: issuableTitleElement.innerHTML,
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
}; };
}, },
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
props: { props: {
canUpdate: this.canUpdate, canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
canMove: this.canMove,
endpoint: this.endpoint, endpoint: this.endpoint,
issuableRef: this.issuableRef, issuableRef: this.issuableRef,
initialTitle: this.initialTitle, initialTitleHtml: this.initialTitleHtml,
initialTitleText: this.initialTitleText,
initialDescriptionHtml: this.initialDescriptionHtml, initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText, initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
markdownPreviewUrl: this.markdownPreviewUrl,
markdownDocs: this.markdownDocs,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
}, },
}); });
}, },
})); });
});
...@@ -4,7 +4,7 @@ export default { ...@@ -4,7 +4,7 @@ export default {
this.preAnimation = true; this.preAnimation = true;
this.pulseAnimation = false; this.pulseAnimation = false;
this.$nextTick(() => { setTimeout(() => {
this.preAnimation = false; this.preAnimation = false;
this.pulseAnimation = true; this.pulseAnimation = true;
}); });
......
import eventHub from '../event_hub';
export default {
methods: {
updateIssuable() {
this.formState.updateLoading = true;
eventHub.$emit('update.issuable');
},
},
};
...@@ -7,10 +7,23 @@ export default class Service { ...@@ -7,10 +7,23 @@ export default class Service {
constructor(endpoint) { constructor(endpoint) {
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint); this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
realtimeChanges: {
method: 'GET',
url: `${this.endpoint}/realtime_changes`,
},
});
} }
getData() { getData() {
return this.resource.get(); return this.resource.realtimeChanges();
}
deleteIssuable() {
return this.resource.delete();
}
updateIssuable(data) {
return this.resource.update(data);
} }
} }
export default class Store { export default class Store {
constructor({ constructor({
titleHtml, titleHtml,
titleText,
descriptionHtml, descriptionHtml,
descriptionText, descriptionText,
}) { }) {
this.state = { this.state = {
titleHtml, titleHtml,
titleText: '', titleText,
descriptionHtml, descriptionHtml,
descriptionText, descriptionText,
taskStatus: '', taskStatus: '',
updatedAt: '', updatedAt: '',
}; };
this.formState = {
title: '',
confidential: false,
description: '',
lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false,
};
} }
updateState(data) { updateState(data) {
...@@ -22,4 +31,15 @@ export default class Store { ...@@ -22,4 +31,15 @@ export default class Store {
this.state.taskStatus = data.task_status; this.state.taskStatus = data.task_status;
this.state.updatedAt = data.updated_at; this.state.updatedAt = data.updated_at;
} }
stateShouldUpdate(data) {
return {
title: this.state.titleText !== data.title_text,
description: this.state.descriptionText !== data.description_text,
};
}
setFormState(state) {
this.formState = Object.assign(this.formState, state);
}
} }
...@@ -170,7 +170,7 @@ gl.text.init = function(form) { ...@@ -170,7 +170,7 @@ gl.text.init = function(form) {
}); });
}; };
gl.text.removeListeners = function(form) { gl.text.removeListeners = function(form) {
return $('.js-md', form).off(); return $('.js-md', form).off('click');
}; };
gl.text.humanize = function(string) { gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
......
...@@ -77,7 +77,9 @@ import './shortcuts_navigation'; ...@@ -77,7 +77,9 @@ import './shortcuts_navigation';
ShortcutsIssuable.prototype.editIssue = function() { ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn; var $editBtn;
$editBtn = $('.issuable-edit'); $editBtn = $('.issuable-edit');
return gl.utils.visitUrl($editBtn.attr('href')); // Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
$editBtn.get(0).click();
}; };
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
......
/* global Flash */ /* global Flash */
import 'vendor/task_list'; import 'deckar01-task_list';
class TaskList { class TaskList {
constructor(options = {}) { constructor(options = {}) {
......
<script>
/* global Flash */
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
export default {
props: {
markdownPreviewUrl: {
type: String,
required: false,
default: '',
},
markdownDocs: {
type: String,
required: true,
},
},
data() {
return {
markdownPreview: '',
markdownPreviewLoading: false,
previewMarkdown: false,
};
},
components: {
markdownHeader,
markdownToolbar,
},
methods: {
toggleMarkdownPreview() {
this.previewMarkdown = !this.previewMarkdown;
if (!this.previewMarkdown) {
this.markdownPreview = '';
} else {
this.markdownPreviewLoading = true;
this.$http.post(
this.markdownPreviewUrl,
{
/*
Can't use `$refs` as the component is technically in the parent component
so we access the VNode & then get the element
*/
text: this.$slots.textarea[0].elm.value,
},
)
.then((res) => {
const data = res.json();
this.markdownPreviewLoading = false;
this.markdownPreview = data.body;
this.$nextTick(() => {
$(this.$refs['markdown-preview']).renderGFM();
});
})
.catch(() => new Flash('Error loading markdown preview'));
}
},
},
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
return new gl.GLForm($(this.$refs['gl-form']), true);
},
};
</script>
<template>
<div
class="md-area prepend-top-default append-bottom-default js-vue-markdown-field"
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
@toggle-markdown="toggleMarkdownPreview" />
<div
class="md-write-holder"
v-show="!previewMarkdown">
<div class="zen-backdrop">
<slot name="textarea"></slot>
<a
class="zen-control zen-control-leave js-zen-leave"
href="#"
aria-label="Enter zen mode">
<i
class="fa fa-compress"
aria-hidden="true">
</i>
</a>
<markdown-toolbar
:markdown-docs="markdownDocs" />
</div>
</div>
<div
class="md md-preview-holder md-preview"
v-show="previewMarkdown">
<div
ref="markdown-preview"
v-html="markdownPreview">
</div>
<span v-if="markdownPreviewLoading">
Loading...
</span>
</div>
</div>
</template>
<script>
import tooltipMixin from '../../mixins/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
components: {
toolbarButton,
},
methods: {
toggleMarkdownPreview(e, form) {
if (form && !form.find('.js-vue-markdown-field').length) {
return;
} else if (e.target.blur) {
e.target.blur();
}
this.$emit('toggle-markdown');
},
},
mounted() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
$(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
},
beforeDestroy() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
$(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
},
};
</script>
<template>
<div class="md-header">
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }">
<a
href="#md-write-holder"
tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)">
Write
</a>
</li>
<li :class="{ active: previewMarkdown }">
<a
href="#md-preview-holder"
tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)">
Preview
</a>
</li>
<li class="pull-right">
<div class="toolbar-group">
<toolbar-button
tag="**"
button-title="Add bold text"
icon="bold" />
<toolbar-button
tag="*"
button-title="Add italic text"
icon="italic" />
<toolbar-button
tag="> "
:prepend="true"
button-title="Insert a quote"
icon="quote-right" />
<toolbar-button
tag="`"
tag-block="```"
button-title="Insert code"
icon="code" />
<toolbar-button
tag="* "
:prepend="true"
button-title="Add a bullet list"
icon="list-ul" />
<toolbar-button
tag="1. "
:prepend="true"
button-title="Add a numbered list"
icon="list-ol" />
<toolbar-button
tag="* [ ] "
:prepend="true"
button-title="Add a task list"
icon="check-square-o" />
</div>
<div class="toolbar-group">
<button
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button"
ref="tooltip">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">
</i>
</button>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
markdownDocs: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
<a
:href="markdownDocs"
target="_blank"
tabindex="-1">
Markdown is supported
</a>
</div>
<button
class="toolbar-button markdown-selector"
type="button"
tabindex="-1">
<i
class="fa fa-file-image-o toolbar-button-icon"
aria-hidden="true">
</i>
Attach a file
</button>
</div>
</template>
<script>
import tooltipMixin from '../../mixins/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
tag: {
type: String,
required: true,
},
tagBlock: {
type: String,
required: false,
default: '',
},
prepend: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
iconClass() {
return `fa-${this.icon}`;
},
},
};
</script>
<template>
<button
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle">
<i
aria-hidden="true"
class="fa fa-fw"
:class="iconClass">
</i>
</button>
</template>
...@@ -6,4 +6,8 @@ export default { ...@@ -6,4 +6,8 @@ export default {
updated() { updated() {
$(this.$refs.tooltip).tooltip('fixTitle'); $(this.$refs.tooltip).tooltip('fixTitle');
}, },
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
}; };
...@@ -475,4 +475,5 @@ ...@@ -475,4 +475,5 @@
.filter-dropdown-loading { .filter-dropdown-loading {
padding: 8px 16px; padding: 8px 16px;
text-align: center;
} }
...@@ -293,7 +293,7 @@ $btn-white-active: #848484; ...@@ -293,7 +293,7 @@ $btn-white-active: #848484;
/* /*
* Badges * Badges
*/ */
$badge-bg: #eee; $badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary; $badge-color: $gl-text-color-secondary;
/* /*
......
...@@ -550,13 +550,13 @@ ul.notes { ...@@ -550,13 +550,13 @@ ul.notes {
position: relative; position: relative;
top: -2px; top: -2px;
display: inline-block; display: inline-block;
padding-left: 4px; padding-left: 7px;
padding-right: 4px; padding-right: 7px;
color: $notes-role-color; color: $notes-role-color;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-base; border-radius: $label-border-radius;
} }
......
class Admin::BuildsController < Admin::ApplicationController class Admin::JobsController < Admin::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@all_builds = Ci::Build @all_builds = Ci::Build
...@@ -20,6 +20,6 @@ class Admin::BuildsController < Admin::ApplicationController ...@@ -20,6 +20,6 @@ class Admin::BuildsController < Admin::ApplicationController
def cancel_all def cancel_all
Ci::Build.running_or_pending.each(&:cancel) Ci::Build.running_or_pending.each(&:cancel)
redirect_to admin_builds_path redirect_to admin_jobs_path
end end
end end
...@@ -14,7 +14,16 @@ module IssuableActions ...@@ -14,7 +14,16 @@ module IssuableActions
name = issuable.human_class_name name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted." flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
respond_to do |format|
format.html { redirect_to index_path }
format.json do
render json: {
web_url: index_path
}
end
end
end end
def bulk_update def bulk_update
......
...@@ -24,7 +24,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -24,7 +24,7 @@ class DashboardController < Dashboard::ApplicationController
def load_events def load_events
projects = projects =
if params[:filter] == "starred" if params[:filter] == "starred"
current_user.viewable_starred_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
else else
current_user.authorized_projects current_user.authorized_projects
end end
......
...@@ -46,7 +46,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -46,7 +46,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
def keep def keep
build.keep_artifacts! build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build) redirect_to namespace_project_job_path(project.namespace, project, build)
end end
def latest_succeeded def latest_succeeded
...@@ -79,7 +79,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -79,7 +79,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def build_from_id def build_from_id
project.builds.find_by(id: params[:build_id]) if params[:build_id] project.builds.find_by(id: params[:job_id]) if params[:job_id]
end end
def build_from_ref def build_from_ref
......
class Projects::BuildArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
before_action :authorize_read_build!
before_action :extract_ref_name_and_path
before_action :validate_artifacts!
def download
redirect_to download_namespace_project_job_artifacts_path(project.namespace, project, job)
end
def browse
redirect_to browse_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
end
def file
redirect_to file_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
end
def raw
redirect_to raw_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
end
def latest_succeeded
redirect_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, job, ref_name_and_path: params[:ref_name_and_path], job: params[:job])
end
private
def validate_artifacts!
render_404 unless job && job.artifacts?
end
def extract_ref_name_and_path
return unless params[:ref_name_and_path]
@ref_name, @path = extract_ref(params[:ref_name_and_path])
end
def job
@job ||= job_from_id || job_from_ref
end
def job_from_id
project.builds.find_by(id: params[:build_id]) if params[:build_id]
end
def job_from_ref
return unless @ref_name
jobs = project.latest_successful_builds_for(@ref_name)
jobs.find_by(name: params[:job])
end
end
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
layout 'project'
def index def index
@scope = params[:scope] redirect_to namespace_project_jobs_path(project.namespace, project)
@all_builds = project.builds.relevant
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
when 'pending'
@builds.pending.reverse_order
when 'running'
@builds.running.reverse_order
when 'finished'
@builds.finished
else
@builds
end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30)
end
def cancel_all
return access_denied! unless can?(current_user, :update_build, project)
@project.builds.running_or_pending.each do |build|
build.cancel if can?(current_user, :update_build, build)
end
redirect_to namespace_project_builds_path(project.namespace, project)
end end
def show def show
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') redirect_to namespace_project_job_path(project.namespace, project, job)
@builds = @builds.where("id not in (?)", @build.id)
@pipeline = @build.pipeline
end
def trace
build.trace.read do |stream|
respond_to do |format|
format.json do
result = {
id: @build.id, status: @build.status, complete: @build.complete?
}
if stream.valid?
stream.limit
state = params[:state].presence
trace = stream.html_with_state(state)
result.merge!(trace.to_h)
end
render json: result
end
end
end
end
def retry
return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
def play
return respond_422 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel
return respond_422 unless @build.cancelable?
@build.cancel
redirect_to build_path(@build)
end
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
.represent_status(@build)
end
def erase
if @build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build),
notice: "Build has been successfully erased!"
else
respond_422
end
end end
def raw def raw
build.trace.read do |stream| redirect_to raw_namespace_project_job_path(project.namespace, project, job)
if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
end
end end
private private
def authorize_update_build! def job
return access_denied! unless can?(current_user, :update_build, build) @job ||= project.builds.find(params[:id])
end
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
end
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end end
end end
...@@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
format.json do format.json do
if @issue.valid? if @issue.valid?
render json: @issue.to_json(methods: [:task_status, :task_status_short], render json: IssueSerializer.new.represent(@issue)
include: { milestone: {},
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { methods: :text_color } })
else else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end end
......
class Projects::JobsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
layout 'project'
def index
@scope = params[:scope]
@all_builds = project.builds.relevant
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
when 'pending'
@builds.pending.reverse_order
when 'running'
@builds.running.reverse_order
when 'finished'
@builds.finished
else
@builds
end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30)
end
def cancel_all
return access_denied! unless can?(current_user, :update_build, project)
@project.builds.running_or_pending.each do |build|
build.cancel if can?(current_user, :update_build, build)
end
redirect_to namespace_project_jobs_path(project.namespace, project)
end
def show
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id)
@pipeline = @build.pipeline
end
def trace
build.trace.read do |stream|
respond_to do |format|
format.json do
result = {
id: @build.id, status: @build.status, complete: @build.complete?
}
if stream.valid?
stream.limit
state = params[:state].presence
trace = stream.html_with_state(state)
result.merge!(trace.to_h)
end
render json: result
end
end
end
end
def retry
return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
def play
return respond_422 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel
return respond_422 unless @build.cancelable?
@build.cancel
redirect_to build_path(@build)
end
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
.represent_status(@build)
end
def erase
if @build.erase(erased_by: current_user)
redirect_to namespace_project_job_path(project.namespace, project, @build),
notice: "Build has been successfully erased!"
else
respond_422
end
end
def raw
build.trace.read do |stream|
if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
end
end
private
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, build)
end
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
end
def build_path(build)
namespace_project_job_path(build.project.namespace, build.project, build)
end
end
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
# project_ids_relation: int[] - project ids to use # project_ids_relation: int[] - project ids to use
# params: # params:
# trending: boolean # trending: boolean
# owned: boolean
# non_public: boolean # non_public: boolean
# starred: boolean # starred: boolean
# sort: string # sort: string
...@@ -28,13 +29,17 @@ class ProjectsFinder < UnionFinder ...@@ -28,13 +29,17 @@ class ProjectsFinder < UnionFinder
def execute def execute
items = init_collection items = init_collection
items = by_ids(items) items = items.map do |item|
item = by_ids(item)
item = by_personal(item)
item = by_starred(item)
item = by_trending(item)
item = by_visibilty_level(item)
item = by_tags(item)
item = by_search(item)
by_archived(item)
end
items = union(items) items = union(items)
items = by_personal(items)
items = by_visibilty_level(items)
items = by_tags(items)
items = by_search(items)
items = by_archived(items)
sort(items) sort(items)
end end
...@@ -43,10 +48,8 @@ class ProjectsFinder < UnionFinder ...@@ -43,10 +48,8 @@ class ProjectsFinder < UnionFinder
def init_collection def init_collection
projects = [] projects = []
if params[:trending].present? if params[:owned].present?
projects << Project.trending projects << current_user.owned_projects if current_user
elsif params[:starred].present? && current_user
projects << current_user.viewable_starred_projects
else else
projects << current_user.authorized_projects if current_user projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
...@@ -56,7 +59,7 @@ class ProjectsFinder < UnionFinder ...@@ -56,7 +59,7 @@ class ProjectsFinder < UnionFinder
end end
def by_ids(items) def by_ids(items)
project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items project_ids_relation ? items.where(id: project_ids_relation) : items
end end
def union(items) def union(items)
...@@ -67,6 +70,14 @@ class ProjectsFinder < UnionFinder ...@@ -67,6 +70,14 @@ class ProjectsFinder < UnionFinder
(params[:personal].present? && current_user) ? items.personal(current_user) : items (params[:personal].present? && current_user) ? items.personal(current_user) : items
end end
def by_starred(items)
(params[:starred].present? && current_user) ? items.starred_by(current_user) : items
end
def by_trending(items)
params[:trending].present? ? items.trending : items
end
def by_visibilty_level(items) def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end end
......
...@@ -276,7 +276,7 @@ module ApplicationHelper ...@@ -276,7 +276,7 @@ module ApplicationHelper
end end
def show_user_callout? def show_user_callout?
cookies[:user_callout_dismissed] == 'true' cookies[:user_callout_dismissed].nil?
end end
def linkedin_url(user) def linkedin_url(user)
......
...@@ -120,7 +120,7 @@ module BlobHelper ...@@ -120,7 +120,7 @@ module BlobHelper
def blob_raw_url def blob_raw_url
if @build && @entry if @build && @entry
raw_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: @entry.path) raw_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: @entry.path)
elsif @snippet elsif @snippet
if @snippet.project_id if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
......
...@@ -2,7 +2,7 @@ module BuildsHelper ...@@ -2,7 +2,7 @@ module BuildsHelper
def build_summary(build, skip: false) def build_summary(build, skip: false)
if build.has_trace? if build.has_trace?
if skip if skip
link_to "View job trace", pipeline_build_url(build.pipeline, build) link_to "View job trace", pipeline_job_url(build.pipeline, build)
else else
build.trace.html(last_lines: 10).html_safe build.trace.html(last_lines: 10).html_safe
end end
...@@ -20,8 +20,8 @@ module BuildsHelper ...@@ -20,8 +20,8 @@ module BuildsHelper
def javascript_build_options def javascript_build_options
{ {
page_url: namespace_project_build_url(@project.namespace, @project, @build), page_url: namespace_project_job_url(@project.namespace, @project, @build),
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_url: namespace_project_job_url(@project.namespace, @project, @build, :json),
build_status: @build.status, build_status: @build.status,
build_stage: @build.stage, build_stage: @build.stage,
log_state: '' log_state: ''
...@@ -31,7 +31,7 @@ module BuildsHelper ...@@ -31,7 +31,7 @@ module BuildsHelper
def build_failed_issue_options def build_failed_issue_options
{ {
title: "Build Failed ##{@build.id}", title: "Build Failed ##{@build.id}",
description: namespace_project_build_url(@project.namespace, @project, @build) description: namespace_project_job_url(@project.namespace, @project, @build)
} }
end end
end end
...@@ -50,8 +50,8 @@ module GitlabRoutingHelper ...@@ -50,8 +50,8 @@ module GitlabRoutingHelper
namespace_project_cycle_analytics_path(project.namespace, project, *args) namespace_project_cycle_analytics_path(project.namespace, project, *args)
end end
def project_builds_path(project, *args) def project_jobs_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args) namespace_project_jobs_path(project.namespace, project, *args)
end end
def project_ref_path(project, ref_name, *args) def project_ref_path(project, ref_name, *args)
...@@ -110,8 +110,8 @@ module GitlabRoutingHelper ...@@ -110,8 +110,8 @@ module GitlabRoutingHelper
namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args) namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
end end
def pipeline_build_url(pipeline, build, *args) def pipeline_job_url(pipeline, build, *args)
namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args) namespace_project_job_url(pipeline.project.namespace, pipeline.project, build.id, *args)
end end
def commits_url(entity, *args) def commits_url(entity, *args)
...@@ -215,13 +215,13 @@ module GitlabRoutingHelper ...@@ -215,13 +215,13 @@ module GitlabRoutingHelper
case action case action
when 'download' when 'download'
download_namespace_project_build_artifacts_path(*args) download_namespace_project_job_artifacts_path(*args)
when 'browse' when 'browse'
browse_namespace_project_build_artifacts_path(*args) browse_namespace_project_job_artifacts_path(*args)
when 'file' when 'file'
file_namespace_project_build_artifacts_path(*args) file_namespace_project_job_artifacts_path(*args)
when 'raw' when 'raw'
raw_namespace_project_build_artifacts_path(*args) raw_namespace_project_job_artifacts_path(*args)
end end
end end
......
...@@ -199,6 +199,27 @@ module IssuablesHelper ...@@ -199,6 +199,27 @@ module IssuablesHelper
issuable_filter_params.any? { |k| params.key?(k) } issuable_filter_params.any? { |k| params.key?(k) }
end end
def issuable_initial_data(issuable)
{
endpoint: namespace_project_issue_path(@project.namespace, @project, issuable),
canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable),
canMove: current_user ? issuable.can_move?(current_user) : false,
issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
markdownPreviewUrl: preview_markdown_path(@project),
markdownDocs: help_page_path('user/markdown'),
projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id),
issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
initialDescriptionText: issuable.description
}.to_json
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -51,6 +51,12 @@ module Ci ...@@ -51,6 +51,12 @@ module Ci
after_destroy :update_project_statistics after_destroy :update_project_statistics
class << self class << self
# This is needed for url_for to work,
# as the controller is JobsController
def model_name
ActiveModel::Name.new(self, nil, 'job')
end
def first_pending def first_pending
pending.unstarted.order('created_at ASC').first pending.unstarted.order('created_at ASC').first
end end
......
...@@ -24,6 +24,10 @@ module Ci ...@@ -24,6 +24,10 @@ module Ci
owner == current_user owner == current_user
end end
def own!(user)
update(owner: user)
end
def inactive? def inactive?
!active? !active?
end end
......
...@@ -242,6 +242,7 @@ class Project < ActiveRecord::Base ...@@ -242,6 +242,7 @@ class Project < ActiveRecord::Base
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) } scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :non_archived, -> { where(archived: false) } scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
...@@ -350,10 +351,6 @@ class Project < ActiveRecord::Base ...@@ -350,10 +351,6 @@ class Project < ActiveRecord::Base
where("projects.id IN (#{union.to_sql})") where("projects.id IN (#{union.to_sql})")
end end
def search_by_visibility(level)
where(visibility_level: Gitlab::VisibilityLevel.string_options[level])
end
def search_by_title(query) def search_by_title(query)
pattern = "%#{query}%" pattern = "%#{query}%"
table = Project.arel_table table = Project.arel_table
......
...@@ -557,12 +557,6 @@ class User < ActiveRecord::Base ...@@ -557,12 +557,6 @@ class User < ActiveRecord::Base
authorized_projects(Gitlab::Access::REPORTER).where(id: projects) authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
end end
def viewable_starred_projects
starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
[Project::PUBLIC, Project::INTERNAL],
authorized_projects.select(:project_id))
end
def owned_projects def owned_projects
@owned_projects ||= @owned_projects ||=
Project.where('namespace_id IN (?) OR namespace_id = ?', Project.where('namespace_id IN (?) OR namespace_id = ?',
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
!::Gitlab::UserAccess !::Gitlab::UserAccess
.new(user, project: build.project) .new(user, project: build.project)
.can_push_to_branch?(build.ref) .can_merge_to_branch?(build.ref)
end end
end end
end end
...@@ -25,7 +25,7 @@ class AnalyticsBuildEntity < Grape::Entity ...@@ -25,7 +25,7 @@ class AnalyticsBuildEntity < Grape::Entity
end end
expose :url do |build| expose :url do |build|
url_to(:namespace_project_build, build) url_to(:namespace_project_job, build)
end end
expose :commit_url do |build| expose :commit_url do |build|
......
...@@ -6,7 +6,7 @@ class BuildActionEntity < Grape::Entity ...@@ -6,7 +6,7 @@ class BuildActionEntity < Grape::Entity
end end
expose :path do |build| expose :path do |build|
play_namespace_project_build_path( play_namespace_project_job_path(
build.project.namespace, build.project.namespace,
build.project, build.project,
build) build)
......
...@@ -6,7 +6,7 @@ class BuildArtifactEntity < Grape::Entity ...@@ -6,7 +6,7 @@ class BuildArtifactEntity < Grape::Entity
end end
expose :path do |build| expose :path do |build|
download_namespace_project_build_artifacts_path( download_namespace_project_job_artifacts_path(
build.project.namespace, build.project.namespace,
build.project, build.project,
build) build)
......
...@@ -5,15 +5,15 @@ class BuildEntity < Grape::Entity ...@@ -5,15 +5,15 @@ class BuildEntity < Grape::Entity
expose :name expose :name
expose :build_path do |build| expose :build_path do |build|
path_to(:namespace_project_build, build) path_to(:namespace_project_job, build)
end end
expose :retry_path do |build| expose :retry_path do |build|
path_to(:retry_namespace_project_build, build) path_to(:retry_namespace_project_job, build)
end end
expose :play_path, if: -> (*) { playable? } do |build| expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_build, build) path_to(:play_namespace_project_job, build)
end end
expose :playable?, as: :playable expose :playable?, as: :playable
......
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
include RequestAwareEntity
expose :branch_name expose :branch_name
expose :confidential expose :confidential
expose :assignees, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
...@@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity ...@@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity
expose :project_id expose :project_id
expose :milestone, using: API::Entities::Milestone expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
expose :web_url do |issue|
namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
end end
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
%span %span
Groups Groups
= nav_link path: 'builds#index' do = nav_link path: 'builds#index' do
= link_to admin_builds_path, title: 'Jobs' do = link_to admin_jobs_path, title: 'Jobs' do
%span %span
Jobs Jobs
= nav_link path: ['runners#index', 'runners#show'] do = nav_link path: ['runners#index', 'runners#show'] do
......
...@@ -4,15 +4,15 @@ ...@@ -4,15 +4,15 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
- build_path_proc = ->(scope) { admin_builds_path(scope: scope) } - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls .nav-controls
- if @all_builds.running_or_pending.any? - if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post = link_to 'Cancel all', cancel_all_admin_jobs_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block .row-content-block.second-block
#{(@scope || 'all').capitalize} jobs #{(@scope || 'all').capitalize} jobs
%ul.content-list.builds-content-list.admin-builds-table %ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true = render "projects/jobs/table", builds: @builds, admin: true
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
%tr.build %tr.build
%td.id %td.id
- if project - if project
= link_to namespace_project_build_path(project.namespace, project, build) do = link_to namespace_project_job_path(project.namespace, project, build) do
%strong ##{build.id} %strong ##{build.id}
- else - else
%strong ##{build.id} %strong ##{build.id}
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= render "projects/last_push" = render "projects/last_push"
%div{ class: container_class } %div{ class: container_class }
- unless show_user_callout? - if show_user_callout?
= render 'shared/user_callout' = render 'shared/user_callout'
- if @projects.any? || params[:name] - if @projects.any? || params[:name]
......
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
-# Shortcut to Pipelines > Jobs -# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds - if project_nav_tab? :builds
%li.hidden %li.hidden
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
Jobs Jobs
-# Shortcut to commits page -# Shortcut to commits page
......
%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" } %a{ href: pipeline_job_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" }
= build.name = build.name
Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) Job #<%= build.id %> ( <%= pipeline_job_url(pipeline, build) %> )
- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path) - path_to_directory = browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: directory.path)
%tr.tree-item{ 'data-link' => path_to_directory } %tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name %td.tree-item-file-name
......
- path_to_file = file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path) - path_to_file = file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: file.path)
%tr.tree-item{ 'data-link' => path_to_file } %tr.tree-item{ 'data-link' => path_to_file }
- blob = file.blob - blob = file.blob
......
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head" = render "projects/pipelines/head"
= render "projects/builds/header", show_controls: false = render "projects/jobs/header", show_controls: false
.tree-holder .tree-holder
.nav-block .nav-block
.tree-controls .tree-controls
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), = link_to download_namespace_project_job_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download') = icon('download')
Download artifacts archive Download artifacts archive
%ul.breadcrumb.repo-breadcrumb %ul.breadcrumb.repo-breadcrumb
%li %li
= link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build) = link_to 'Artifacts', browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path| - path_breadcrumbs do |title, path|
%li %li
= link_to truncate(title, length: 40), browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) = link_to truncate(title, length: 40), browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path)
.tree-content-holder .tree-content-holder
%table.table.tree-table %table.table.tree-table
......
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head" = render "projects/pipelines/head"
= render "projects/builds/header", show_controls: false = render "projects/jobs/header", show_controls: false
#tree-holder.tree-holder #tree-holder.tree-holder
.nav-block .nav-block
%ul.breadcrumb.repo-breadcrumb %ul.breadcrumb.repo-breadcrumb
%li %li
= link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build) = link_to 'Artifacts', browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path| - path_breadcrumbs do |title, path|
- title = truncate(title, length: 40) - title = truncate(title, length: 40)
%li %li
- if path == @path - if path == @path
= link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) do = link_to file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path) do
%strong= title %strong= title
- else - else
= link_to title, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) = link_to title, browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path)
%article.file-holder %article.file-holder
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%td.branch-commit %td.branch-commit
- if can?(current_user, :read_build, job) - if can?(current_user, :read_build, job)
= link_to namespace_project_build_url(job.project.namespace, job.project, job) do = link_to namespace_project_job_url(job.project.namespace, job.project, job) do
%span.build-link ##{job.id} %span.build-link ##{job.id}
- else - else
%span.build-link ##{job.id} %span.build-link ##{job.id}
...@@ -95,16 +95,16 @@ ...@@ -95,16 +95,16 @@
%td %td
.pull-right .pull-right
- if can?(current_user, :read_build, job) && job.artifacts? - if can?(current_user, :read_build, job) && job.artifacts?
= link_to download_namespace_project_build_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = link_to download_namespace_project_job_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download') = icon('download')
- if can?(current_user, :update_build, job) - if can?(current_user, :update_build, job)
- if job.active? - if job.active?
= link_to cancel_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = link_to cancel_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred') = icon('remove', class: 'cred')
- elsif allow_retry - elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job) - if job.playable? && !admin && can?(current_user, :update_build, job)
= link_to play_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = link_to play_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play') = custom_icon('icon_play')
- elsif job.retryable? - elsif job.retryable?
= link_to retry_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do = link_to retry_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
= icon('repeat') = icon('repeat')
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action| - actions.each do |action|
- next unless can?(current_user, :update_build, action)
%li %li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play') = custom_icon('icon_play')
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop? - if can?(current_user, :stop_environment, @environment)
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.environments-container .environments-container
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%ul %ul
- if can_update_issue - if can_update_issue
%li %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'issuable-edit'
%li %li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li %li
...@@ -55,10 +55,8 @@ ...@@ -55,10 +55,8 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
#js-issuable-app{ "data" => { "endpoint" => realtime_changes_namespace_project_issue_path(@project.namespace, @project, @issue), %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue)
"can-update" => can?(current_user, :update_issue, @issue).to_s, #js-issuable-app
"issuable-ref" => @issue.to_reference,
} }
%h2.title= markdown_field(@issue, :title) %h2.title= markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
%strong %strong
Job Job
= link_to "##{@build.id}", namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' = link_to "##{@build.id}", namespace_project_job_path(@project.namespace, @project, @build), class: 'js-build-id'
in pipeline in pipeline
%strong %strong
= link_to "##{pipeline.id}", pipeline_path(pipeline) = link_to "##{pipeline.id}", pipeline_path(pipeline)
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
%strong %strong
= link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name' = link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name'
= render "projects/builds/user" if @build.user = render "projects/jobs/user" if @build.user
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
...@@ -26,6 +26,6 @@ ...@@ -26,6 +26,6 @@
- if can?(current_user, :create_issue, @project) && @build.failed? - if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left') = icon('angle-double-left')
...@@ -30,21 +30,21 @@ ...@@ -30,21 +30,21 @@
- if @build.artifacts? - if @build.artifacts?
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do = link_to keep_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep Keep
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do = link_to download_namespace_project_job_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download Download
- if @build.artifacts_metadata? - if @build.artifacts_metadata?
= link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do = link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
Browse Browse
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title .title
Job details Job details
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request - if @build.merge_request
%p.build-detail-row %p.build-detail-row
%span.build-light-text Merge Request: %span.build-light-text Merge Request:
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
\##{@build.runner.id} \##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
- if @build.active? - if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post = link_to "Cancel", cancel_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- if @build.trigger_request - if @build.trigger_request
.build-widget .build-widget
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
- HasStatus::ORDERED_STATUSES.each do |build_status| - HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build| - builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to namespace_project_build_path(@project.namespace, @project, build) do = link_to namespace_project_job_path(@project.namespace, @project, build) do
= icon('arrow-right') = icon('arrow-right')
%span{ class: "ci-status-icon-#{build.status}" } %span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status) = ci_icon_for_status(build.status)
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
- build_path_proc = ->(scope) { project_builds_path(@project, scope: scope) } - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls .nav-controls
- if can?(current_user, :update_build, @project) - if can?(current_user, :update_build, @project)
- if @all_builds.running_or_pending.any? - if @all_builds.running_or_pending.any?
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), = link_to 'Cancel running', cancel_all_namespace_project_jobs_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
......
...@@ -62,17 +62,17 @@ ...@@ -62,17 +62,17 @@
Showing last Showing last
%span.js-truncated-info-size.truncated-info-size>< %span.js-truncated-info-size.truncated-info-size><
KiB of log - KiB of log -
%a.js-raw-link.raw-link{ href: raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw
.controllers .controllers
- if @build.has_trace? - if @build.has_trace?
= link_to raw_namespace_project_build_path(@project.namespace, @project, @build), = link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
title: 'Open raw trace', title: 'Open raw trace',
data: { placement: 'top', container: 'body' }, data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip' do class: 'js-raw-link-controller has-tooltip' do
= icon('download') = icon('download')
- if can?(current_user, :update_build, @project) && @build.erasable? - if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build), = link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
method: :post, method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase Build', title: 'Erase Build',
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(controller: [:builds, :artifacts]) do = nav_link(controller: [:builds, :artifacts]) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span %span
Jobs Jobs
......
...@@ -51,5 +51,5 @@ ...@@ -51,5 +51,5 @@
%span.stage %span.stage
= build.stage.titleize = build.stage.titleize
%span.build-name %span.build-name
= link_to build.name, pipeline_build_url(pipeline, build) = link_to build.name, pipeline_job_url(pipeline, build)
%pre.build-log= build_summary(build, skip: index >= 10) %pre.build-log= build_summary(build, skip: index >= 10)
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
Snippets Snippets
%div{ class: container_class } %div{ class: container_class }
- if @user == current_user && !show_user_callout? - if @user == current_user && show_user_callout?
= render 'shared/user_callout' = render 'shared/user_callout'
.tab-content .tab-content
#activity.tab-pane #activity.tab-pane
......
---
title: Add API support for pipeline schedule
merge_request: 11307
author: dosuken123
---
title: Add tag_list param to project api
merge_request: 11799
author: Ivan Chernov
---
title: Count badges depend on translucent color to better adjust to different background
colors and permission badges now feature a pill shaped design similar to labels
merge_request:
author:
---
title: Respect merge, instead of push, permissions for protected actions
merge_request: 11648
author:
---
title: Enables inline editing for an issues title & description
merge_request:
author:
---
title: Ask for an example project for bug reports
merge_request:
author:
---
title: Change /builds in the URL to /-/jobs. Backward URLs were also added
merge_request: 11407
author:
---
title: Update task_list to version 2.0.0
merge_request: 11525
author: Jared Deckard <jared.deckard@gmail.com>
---
title: Improve performance of ProjectFinder used in /projects API endpoint
merge_request: 11666
author:
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# - [project.namespace, project, build] # - [project.namespace, project, build]
# #
# instead of: # instead of:
# - namespace_project_build_path(project.namespace, project, build) # - namespace_project_job_path(project.namespace, project, build)
# #
# Without that, Ci:: namespace is used for resolving routes: # Without that, Ci:: namespace is used for resolving routes:
# - namespace_project_ci_build_path(project.namespace, project, build) # - namespace_project_ci_build_path(project.namespace, project, build)
......
...@@ -118,7 +118,7 @@ namespace :admin do ...@@ -118,7 +118,7 @@ namespace :admin do
resources :cohorts, only: :index resources :cohorts, only: :index
resources :builds, only: :index do resources :jobs, only: :index do
collection do collection do
post :cancel_all post :cancel_all
end end
......
require 'constraints/project_url_constrainer' require 'constraints/project_url_constrainer'
require 'gitlab/routes/legacy_builds'
resources :projects, only: [:index, :new, :create] resources :projects, only: [:index, :new, :create]
...@@ -180,7 +181,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -180,7 +181,8 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do scope '-' do
resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do
collection do collection do
post :cancel_all post :cancel_all
...@@ -211,6 +213,9 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -211,6 +213,9 @@ constraints(ProjectUrlConstrainer.new) do
post :keep post :keep
end end
end end
end
Gitlab::Routes::LegacyBuilds.new(self).draw
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do member do
......
...@@ -33,6 +33,7 @@ following locations: ...@@ -33,6 +33,7 @@ following locations:
- [Notification settings](notification_settings.md) - [Notification settings](notification_settings.md)
- [Pipelines](pipelines.md) - [Pipelines](pipelines.md)
- [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Triggers](pipeline_triggers.md)
- [Pipeline Schedules](pipeline_schedules.md)
- [Projects](projects.md) including setting Webhooks - [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md) - [Project Access Requests](access_requests.md)
- [Project Members](members.md) - [Project Members](members.md)
......
# Pipeline schedules
You can read more about [pipeline schedules](../user/project/pipelines/schedules.md).
## Get all pipeline schedules
Get a list of the pipeline schedules of a project.
```
GET /projects/:id/pipeline_schedules
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `scope` | string | no | The scope of pipeline schedules, one of: `active`, `inactive` |
```sh
curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
```
```json
[
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"cron": "* * * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T13:41:00.000Z",
"active": true,
"created_at": "2017-05-19T13:31:08.849Z",
"updated_at": "2017-05-19T13:40:17.727Z",
"owner": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
}
}
]
```
## Get a single pipeline schedule
Get the pipeline schedule of a project.
```
GET /projects/:id/pipeline_schedules/:pipeline_schedule_id
```
| Attribute | Type | required | Description |
|--------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
```sh
curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
```
```json
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"cron": "* * * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T13:41:00.000Z",
"active": true,
"created_at": "2017-05-19T13:31:08.849Z",
"updated_at": "2017-05-19T13:40:17.727Z",
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"status": "pending"
},
"owner": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
}
}
```
## Create a new pipeline schedule
Create a new pipeline schedule of a project.
```
POST /projects/:id/pipeline_schedules
```
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `description` | string | yes | The description of pipeline schedule |
| `ref` | string | yes | The branch/tag name will be triggered |
| `cron ` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
```sh
curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
```
```json
{
"id": 14,
"description": "Build packages",
"ref": "master",
"cron": "0 1 * * 5",
"cron_timezone": "UTC",
"next_run_at": "2017-05-26T01:00:00.000Z",
"active": true,
"created_at": "2017-05-19T13:43:08.169Z",
"updated_at": "2017-05-19T13:43:08.169Z",
"last_pipeline": null,
"owner": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
}
}
```
## Edit a pipeline schedule
Updates the pipeline schedule of a project. Once the update is done, it will be rescheduled automatically.
```
PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
```
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `description` | string | no | The description of pipeline schedule |
| `ref` | string | no | The branch/tag name will be triggered |
| `cron ` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
```sh
curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form cron="0 2 * * *" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
```
```json
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"cron": "0 2 * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T17:00:00.000Z",
"active": true,
"created_at": "2017-05-19T13:31:08.849Z",
"updated_at": "2017-05-19T13:44:16.135Z",
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"status": "pending"
},
"owner": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
}
}
```
## Take ownership of a pipeline schedule
Update the owner of the pipeline schedule of a project.
```
POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership
```
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
```sh
curl --request POST --header "PRIVATE-TOKEN: hf2CvZXB9w8Uc5pZKpSB" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/take_ownership"
```
```json
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"cron": "0 2 * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T17:00:00.000Z",
"active": true,
"created_at": "2017-05-19T13:31:08.849Z",
"updated_at": "2017-05-19T13:46:37.468Z",
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"status": "pending"
},
"owner": {
"name": "shinya",
"username": "maeda",
"id": 50,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/8ca0a796a679c292e3a11da50f99e801?s=80&d=identicon",
"web_url": "https://gitlab.example.com/maeda"
}
}
```
## Delete a pipeline schedule
Delete the pipeline schedule of a project.
```
DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id
```
| Attribute | Type | required | Description |
|----------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
```sh
curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
```
```json
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"cron": "0 2 * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T17:00:00.000Z",
"active": true,
"created_at": "2017-05-19T13:31:08.849Z",
"updated_at": "2017-05-19T13:46:37.468Z",
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"status": "pending"
},
"owner": {
"name": "shinya",
"username": "maeda",
"id": 50,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/8ca0a796a679c292e3a11da50f99e801?s=80&d=identicon",
"web_url": "https://gitlab.example.com/maeda"
}
}
```
...@@ -473,6 +473,7 @@ Parameters: ...@@ -473,6 +473,7 @@ Parameters:
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
### Create project for user ### Create project for user
...@@ -506,6 +507,7 @@ Parameters: ...@@ -506,6 +507,7 @@ Parameters:
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
### Edit project ### Edit project
...@@ -538,6 +540,7 @@ Parameters: ...@@ -538,6 +540,7 @@ Parameters:
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
### Fork project ### Fork project
......
...@@ -591,7 +591,7 @@ Optional manual actions have `allow_failure: true` set by default. ...@@ -591,7 +591,7 @@ Optional manual actions have `allow_failure: true` set by default.
**Manual actions are considered to be write actions, so permissions for **Manual actions are considered to be write actions, so permissions for
protected branches are used when user wants to trigger an action. In other protected branches are used when user wants to trigger an action. In other
words, in order to trigger a manual action assigned to a branch that the words, in order to trigger a manual action assigned to a branch that the
pipeline is running for, user needs to have ability to push to this branch.** pipeline is running for, user needs to have ability to merge to this branch.**
### environment ### environment
...@@ -1105,6 +1105,36 @@ variables: ...@@ -1105,6 +1105,36 @@ variables:
GIT_STRATEGY: none GIT_STRATEGY: none
``` ```
## Git Checkout
> Introduced in GitLab Runner 9.3
The `GIT_CHECKOUT` variable can be used when the `GIT_STRATEGY` is set to either
`clone` or `fetch` to specify whether a `git checkout` should be run. If not
specified, it defaults to true. Like `GIT_STRATEGY`, it can be set in either the
global [`variables`](#variables) section or the [`variables`](#job-variables)
section for individual jobs.
If set to `false`, the Runner will:
- when doing `fetch` - update the repository and leave working copy on
the current revision,
- when doing `clone` - clone the repository and leave working copy on the
default branch.
Having this setting set to `true` will mean that for both `clone` and `fetch`
strategies the Runner will checkout the working copy to a revision related
to the CI pipeline:
```yaml
variables:
GIT_STRATEGY: clone
GIT_CHECKOUT: false
script:
- git checkout master
- git merge $CI_BUILD_REF_NAME
```
## Git Submodule Strategy ## Git Submodule Strategy
> Requires GitLab Runner v1.10+. > Requires GitLab Runner v1.10+.
......
...@@ -27,11 +27,11 @@ module SharedBuilds ...@@ -27,11 +27,11 @@ module SharedBuilds
end end
step 'I visit recent build details page' do step 'I visit recent build details page' do
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_job_path(@project.namespace, @project, @build)
end end
step 'I visit project builds page' do step 'I visit project builds page' do
visit namespace_project_builds_path(@project.namespace, @project) visit namespace_project_jobs_path(@project.namespace, @project)
end end
step 'recent build has artifacts available' do step 'recent build has artifacts available' do
...@@ -56,7 +56,7 @@ module SharedBuilds ...@@ -56,7 +56,7 @@ module SharedBuilds
end end
step 'I access artifacts download page' do step 'I access artifacts download page' do
visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build) visit download_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
end end
step 'I see details of a build' do step 'I see details of a build' do
......
...@@ -30,7 +30,7 @@ module SharedMarkdown ...@@ -30,7 +30,7 @@ module SharedMarkdown
end end
step 'I should see the Markdown write tab' do step 'I should see the Markdown write tab' do
expect(find('.gfm-form')).to have_css('.js-md-write-button', visible: true) expect(first('.gfm-form')).to have_link('Write', visible: true)
end end
step 'I should see the Markdown preview' do step 'I should see the Markdown preview' do
...@@ -49,9 +49,9 @@ module SharedMarkdown ...@@ -49,9 +49,9 @@ module SharedMarkdown
end end
step 'I preview a description text like "Bug fixed :smile:"' do step 'I preview a description text like "Bug fixed :smile:"' do
page.within('.gfm-form') do page.within(first('.gfm-form')) do
fill_in 'Description', with: 'Bug fixed :smile:' fill_in 'Description', with: 'Bug fixed :smile:'
find('.js-md-preview-button').click click_link 'Preview'
end end
end end
......
...@@ -110,6 +110,7 @@ module API ...@@ -110,6 +110,7 @@ module API
mount ::API::Notes mount ::API::Notes
mount ::API::NotificationSettings mount ::API::NotificationSettings
mount ::API::Pipelines mount ::API::Pipelines
mount ::API::PipelineSchedules
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
......
...@@ -687,6 +687,17 @@ module API ...@@ -687,6 +687,17 @@ module API
expose :coverage expose :coverage
end end
class PipelineSchedule < Grape::Entity
expose :id
expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
expose :created_at, :updated_at
expose :owner, using: Entities::UserBasic
end
class PipelineScheduleDetails < PipelineSchedule
expose :last_pipeline, using: Entities::PipelineBasic
end
class EnvironmentBasic < Grape::Entity class EnvironmentBasic < Grape::Entity
expose :id, :name, :slug, :external_url expose :id, :name, :slug, :external_url
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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