Commit 79f40f27 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into deploy-keys-load-async

parents 5c91113c 985737fd
......@@ -264,11 +264,15 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
stage: test
script:
- scripts/static-analysis
docs:check:links:
# Documentation checks:
# - Check validity of relative links
# - Make sure cURL examples in API docs use the full switches
docs lint:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
......@@ -276,6 +280,7 @@ docs:check:links:
dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
......
This diff is collapsed.
# GitLab Contributing Process
## GitLab Core Team & GitLab Inc. Contribution Process
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Purpose of describing the contributing process](#purpose-of-describing-the-contributing-process)
- [Common actions](#common-actions)
- [Merge request coaching](#merge-request-coaching)
- [Assigning issues](#assigning-issues)
- [Be kind](#be-kind)
- [Feature freeze on the 7th for the release on the 22nd](#feature-freeze-on-the-7th-for-the-release-on-the-22nd)
- [Between the 1st and the 7th](#between-the-1st-and-the-7th)
- [On the 7th](#on-the-7th)
- [After the 7th](#after-the-7th)
- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
- [Retrospective](#retrospective)
- [Kickoff](#kickoff)
- [Copy & paste responses](#copy--paste-responses)
- [Improperly formatted issue](#improperly-formatted-issue)
- [Issue report for old version](#issue-report-for-old-version)
- [Support requests and configuration questions](#support-requests-and-configuration-questions)
- [Code format](#code-format)
- [Issue fixed in newer version](#issue-fixed-in-newer-version)
- [Improperly formatted merge request](#improperly-formatted-merge-request)
- [Inactivity close of an issue](#inactivity-close-of-an-issue)
- [Inactivity close of a merge request](#inactivity-close-of-a-merge-request)
- [Accepting merge requests](#accepting-merge-requests)
- [Only accepting merge requests with green tests](#only-accepting-merge-requests-with-green-tests)
- [Closing down the issue tracker on GitHub](#closing-down-the-issue-tracker-on-github)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
---
## Purpose of describing the contributing process
Below we describe the contributing process to GitLab for two reasons. So that
contributors know what to expect from maintainers (possible responses, friendly
treatment, etc.). And so that maintainers know what to expect from contributors
(use the latest version, ensure that the issue is addressed, friendly treatment,
etc.).
Below we describe the contributing process to GitLab for two reasons:
1. Contributors know what to expect from maintainers (possible responses, friendly
treatment, etc.)
1. Maintainers know what to expect from contributors (use the latest version,
ensure that the issue is addressed, friendly treatment, etc.).
- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
## Common actions
### Issue triaging
Our issue triage policies are [described in our handbook]. You are very welcome
to help the GitLab team triage issues. We also organize [issue bash events] once
every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from
[GitLab team][team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get
......@@ -37,12 +55,6 @@ their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
## Workflow labels
Labelling issues is described in the [GitLab Inc engineering workflow].
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
## Assigning issues
If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover.
......@@ -146,6 +158,29 @@ release should have the correct milestone assigned _and_ have the label
Merge requests without a milestone and this label will
not be merged into any stable branches.
## Release retrospective and kickoff
### Retrospective
After each release, we have a retrospective call where we discuss what went well,
what went wrong, and what we can improve for the next release. The
[retrospective notes] are public and you are invited to comment on them.
If you're interested, you can even join the
[retrospective call][retro-kickoff-call], on the first working day after the
22nd at 6pm CET / 9am PST.
### Kickoff
Before working on the next release, we have a
kickoff call to explain what we expect to ship in the next release. The
[kickoff notes] are public and you are invited to comment on them.
If you're interested, you can even join the [kickoff call][retro-kickoff-call],
on the first working day after the 7th at 6pm CET / 9am PST..
[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
## Copy & paste responses
### Improperly formatted issue
......
......@@ -46,6 +46,7 @@ export default Vue.component('pipelines-table', {
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
};
},
......@@ -130,15 +131,21 @@ export default Vue.component('pipelines-table', {
const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines);
this.isLoading = false;
this.updateGraphDropdown = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
},
},
......@@ -163,7 +170,9 @@ export default Vue.component('pipelines-table', {
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service" />
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
`,
......
/* eslint-disable no-new */
/* global Flash */
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch';
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
this.availableButton = this.wrapperEl.querySelector('.available');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
this.createMrPath = this.wrapperEl.dataset.createMrPath;
this.droplabInitialized = false;
this.isCreatingMergeRequest = false;
this.mergeRequestCreated = false;
this.isCreatingBranch = false;
this.branchCreated = false;
this.init();
}
init() {
this.checkAbilityToCreateBranch();
}
available() {
this.availableButton.classList.remove('hide');
this.unavailableButton.classList.add('hide');
}
unavailable() {
this.availableButton.classList.add('hide');
this.unavailableButton.classList.remove('hide');
}
enable() {
this.createMergeRequestButton.classList.remove('disabled');
this.createMergeRequestButton.removeAttribute('disabled');
this.dropdownToggle.classList.remove('disabled');
this.dropdownToggle.removeAttribute('disabled');
}
disable() {
this.createMergeRequestButton.classList.add('disabled');
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
this.dropdownToggle.classList.add('disabled');
this.dropdownToggle.setAttribute('disabled', 'disabled');
}
hide() {
this.wrapperEl.classList.add('hide');
}
setUnavailableButtonState(isLoading = true) {
if (isLoading) {
this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
this.unavailableButtonText.textContent = 'Checking branch availability…';
} else {
this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
this.unavailableButtonText.textContent = 'New branch unavailable';
}
}
checkAbilityToCreateBranch() {
return $.ajax({
type: 'GET',
dataType: 'json',
url: this.canCreatePath,
beforeSend: () => this.setUnavailableButtonState(),
})
.done((data) => {
this.setUnavailableButtonState(false);
if (data.can_create_branch) {
this.available();
this.enable();
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
this.hide();
}
}).fail(() => {
this.unavailable();
this.disable();
new Flash('Failed to check if a new branch can be created.');
});
}
initDroplab() {
this.droplab = new DropLab();
this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
this.getDroplabConfig());
}
getDroplabConfig() {
return {
InputSetter: [{
input: this.createMergeRequestButton,
valueAttribute: 'data-value',
inputAttribute: 'data-action',
}, {
input: this.createMergeRequestButton,
valueAttribute: 'data-text',
}],
};
}
bindEvents() {
this.createMergeRequestButton
.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
}
isBusy() {
return this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
this.branchCreated;
}
onClickCreateMergeRequestButton(e) {
let xhr = null;
e.preventDefault();
if (this.isBusy()) {
return;
}
if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
xhr = this.createMergeRequest();
} else if (e.target.dataset.action === CREATE_BRANCH) {
xhr = this.createBranch();
}
xhr.fail(() => {
this.isCreatingMergeRequest = false;
this.isCreatingBranch = false;
});
xhr.always(() => this.enable());
this.disable();
}
createMergeRequest() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createMrPath,
beforeSend: () => (this.isCreatingMergeRequest = true),
})
.done((data) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
}
createBranch() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createBranchPath,
beforeSend: () => (this.isCreatingBranch = true),
})
.done((data) => {
this.branchCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
}
}
......@@ -5,7 +5,7 @@ require('./preview_markdown');
window.DropzoneInput = (function() {
function DropzoneInput(form) {
var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
Dropzone.autoDiscover = false;
alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
......@@ -16,7 +16,7 @@ window.DropzoneInput = (function() {
iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
project_uploads_path = window.project_uploads_path || null;
uploads_path = window.uploads_path || null;
max_file_size = gon.max_file_size || 10;
form_textarea = $(form).find(".js-gfm-input");
form_textarea.wrap("<div class=\"div-dropzone\"></div>");
......@@ -39,10 +39,10 @@ window.DropzoneInput = (function() {
"display": "none"
});
if (!project_uploads_path) return;
if (!uploads_path) return;
dropzone = form_dropzone.dropzone({
url: project_uploads_path,
url: uploads_path,
dictDefaultMessage: "",
clickable: true,
paramName: "file",
......@@ -159,7 +159,7 @@ window.DropzoneInput = (function() {
formData = new FormData();
formData.append("file", item, filename);
return $.ajax({
url: project_uploads_path,
url: uploads_path,
type: "POST",
data: formData,
dataType: "json",
......
<script>
/* eslint-disable no-new */
/* global Flash */
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue';
......@@ -71,11 +69,13 @@ export default {
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
methods: {
......@@ -122,6 +122,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
});
},
......@@ -137,9 +138,16 @@ export default {
})
.catch(() => {
this.isLoadingFolderContent = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
});
},
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.'));
},
},
};
</script>
......@@ -217,7 +225,6 @@ export default {
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
......
<script>
/* global Flash */
/* eslint-disable no-new */
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
......@@ -12,11 +9,6 @@ export default {
required: false,
default: () => [],
},
service: {
type: Object,
required: true,
},
},
data() {
......@@ -38,15 +30,7 @@ export default {
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
eventHub.$emit('postAction', endpoint);
},
isActionDisabled(action) {
......
......@@ -46,11 +46,6 @@ export default {
required: false,
default: false,
},
service: {
type: Object,
required: true,
},
},
computed: {
......@@ -543,31 +538,34 @@ export default {
<actions-component
v-if="hasManualActions && canCreateDeployment"
:service="service"
:actions="manualActions"/>
:actions="manualActions"
/>
<external-url-component
v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
:external-url="externalURL"
/>
<monitoring-button-component
v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/>
:monitoring-url="monitoringUrl"
/>
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
:terminal-path="model.terminal_path"
/>
<stop-component
v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
/>
<rollback-component
v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
:service="service"/>
/>
</div>
</td>
</tr>
......
<script>
/* global Flash */
/* eslint-disable no-new */
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`.
......@@ -20,11 +18,6 @@ export default {
type: Boolean,
default: true,
},
service: {
type: Object,
required: true,
},
},
data() {
......@@ -37,17 +30,7 @@ export default {
onClick() {
this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
eventHub.$emit('postAction', this.retryUrl);
},
},
};
......
<script>
/* global Flash */
/* eslint-disable no-new, no-alert */
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
......@@ -13,11 +11,6 @@ export default {
type: String,
default: '',
},
service: {
type: Object,
required: true,
},
},
data() {
......@@ -34,20 +27,13 @@ export default {
methods: {
onClick() {
// eslint-disable-next-line no-alert
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.', 'alert');
});
eventHub.$emit('postAction', this.stopUrl);
}
},
},
......
......@@ -28,11 +28,6 @@ export default {
default: false,
},
service: {
type: Object,
required: true,
},
isLoadingFolderContent: {
type: Boolean,
required: false,
......@@ -78,7 +73,7 @@ export default {
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service" />
/>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
......@@ -96,7 +91,7 @@ export default {
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service" />
/>
<tr>
<td
......
<script>
/* eslint-disable no-new */
/* global Flash */
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue';
......@@ -99,6 +98,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
......@@ -169,7 +169,7 @@ export default {
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"/>
/>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
......@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
this.addEventListeners();
}
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
/* global Flash */
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
require('./flash');
require('~/lib/utils/text_utility');
......@@ -18,48 +19,49 @@ class Issue {
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
Issue.initIssueBtnEventListeners();
this.initIssueBtnEventListeners();
}
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
Issue.initMergeRequests();
Issue.initRelatedBranches();
Issue.initCanCreateBranch();
if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
}
}
static initIssueBtnEventListeners() {
initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
const closeButtons = $('a.btn-close');
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
const reopenButtons = $('a.btn-reopen');
return closeButtons.add(reopenButtons).on('click', function(e) {
var $this, shouldSubmit, url;
return closeButtons.add(reopenButtons).on('click', (e) => {
var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
$this = $(this);
shouldSubmit = $this.hasClass('btn-comment');
$button = $(e.currentTarget);
shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($this.closest('form'));
Issue.submitNoteForm($button.closest('form'));
}
$this.prop('disabled', true);
Issue.setNewBranchButtonState(true, null);
url = $this.attr('href');
$button.prop('disabled', true);
url = $button.attr('href');
return $.ajax({
type: 'PUT',
url: url
}).fail(function(jqXHR, textStatus, errorThrown) {
new Flash(issueFailMessage);
Issue.initCanCreateBranch();
}).done(function(data, textStatus, jqXHR) {
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
if ('id' in data) {
$(document).trigger('issuable:change');
const isClosed = $this.hasClass('btn-close');
const isClosed = $button.hasClass('btn-close');
closeButtons.toggleClass('hidden', isClosed);
reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed);
......@@ -68,12 +70,21 @@ class Issue {
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
this.createMergeRequestDropdown.unavailable();
this.createMergeRequestDropdown.disable();
} else {
// We should check in case a branch was created in another tab
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
}
} else {
new Flash(issueFailMessage);
}
$this.prop('disabled', false);
Issue.initCanCreateBranch();
$button.prop('disabled', false);
});
});
}
......@@ -109,29 +120,6 @@ class Issue {
}
});
}
static initCanCreateBranch() {
// If the user doesn't have the required permissions the container isn't
// rendered at all.
if (Issue.$btnNewBranch.length === 0) {
return;
}
return $.getJSON(Issue.$btnNewBranch.data('path')).fail(function() {
Issue.setNewBranchButtonState(false, false);
new Flash('Failed to check if a new branch can be created.');
}).done(function(data) {
Issue.setNewBranchButtonState(false, data.can_create_branch);
});
}
static setNewBranchButtonState(isPending, canCreate) {
if (Issue.$btnNewBranch.length === 0) {
return;
}
Issue.$btnNewBranch.find('.available').toggle(!isPending && canCreate);
Issue.$btnNewBranch.find('.unavailable').toggle(!isPending && !canCreate);
}
}
export default Issue;
This diff is collapsed.
<script>
/**
* Renders each stage of the pipeline mini graph.
*
* Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked.
*
* Request is made inside this component to make it reusable between:
* 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views
* 3. Merge request widget
* 4. Commit widget
*/
/* global Flash */
import StatusIconEntityMap from '../../ci_status_icons';
......@@ -7,36 +22,55 @@ export default {
type: Object,
required: true,
},
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
isLoading: false,
dropdownContent: '',
endpoint: this.stage.dropdown_path,
};
},
updated() {
if (this.builds) {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
watch: {
updateDropdown() {
if (this.updateDropdown &&
this.isDropdownOpen() &&
!this.isLoading) {
this.fetchJobs();
}
},
},
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
this.isLoading = true;
this.fetchJobs();
}
},
return this.$http.get(this.stage.dropdown_path)
fetchJobs() {
this.$http.get(this.endpoint)
.then((response) => {
this.builds = JSON.parse(response.body).html;
this.dropdownContent = response.json().html;
this.isLoading = false;
})
.catch(() => {
// If dropdown is opened we'll close it.
if (this.$el.classList.contains('open')) {
$(this.$refs.dropdown).dropdown('toggle');
}
this.closeDropdown();
this.isLoading = false;
const flash = new Flash('Something went wrong on our end.');
return flash;
......@@ -57,59 +91,83 @@ export default {
e.stopPropagation();
});
},
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
return `ci-status-icon-${this.stage.status.group}`;
},
svgHTML() {
svgIcon() {
return StatusIconEntityMap[this.stage.status.icon];
},
},
template: `
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title"
ref="dropdown">
<span
v-html="svgHTML"
aria-hidden="true">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true" />
</button>
<ul
ref="dropdown-content"
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div
class="arrow-up"
aria-hidden="true"></div>
};
</script>
<template>
<div class="dropdown">
<button
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
id="stageDropdown"
aria-haspopup="true"
aria-expanded="false">
<span
v-html="svgIcon"
aria-hidden="true"
:aria-label="stage.title">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown">
<li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu">
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
class="text-center"
v-if="isLoading">
<i
class="fa fa-spin fa-spinner"
aria-hidden="true"
aria-label="Loading">
</i>
</div>
</ul>
</div>
`,
};
<ul
v-else
v-html="dropdownContent">
</ul>
</li>
</ul>
</div>
</script>
......@@ -49,6 +49,7 @@ export default {
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
};
},
......@@ -198,15 +199,21 @@ export default {
this.store.storePagination(response.headers);
this.isLoading = false;
this.updateGraphDropdown = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
},
},
......@@ -263,7 +270,9 @@ export default {
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
<gl-pagination
......
......@@ -10,13 +10,18 @@ export default {
pipelines: {
type: Array,
required: true,
default: () => ([]),
},
service: {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
......@@ -40,7 +45,9 @@ export default {
v-bind:model="model">
<tr is="pipelines-table-row-component"
:pipeline="model"
:service="service"></tr>
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template>
</tbody>
</table>
......
......@@ -3,7 +3,7 @@ import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../pipelines/components/status';
import PipelinesStageComponent from '../../pipelines/components/stage';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit';
......@@ -24,6 +24,12 @@ export default {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
......@@ -213,7 +219,10 @@ export default {
<div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages">
<dropdown-stage :stage="stage"/>
<dropdown-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"/>
</div>
</td>
......
......@@ -425,12 +425,6 @@
float: right;
}
.diffs {
.content-block {
border-bottom: none;
}
}
.files-changed {
border-bottom: none;
}
......
......@@ -161,3 +161,86 @@ ul.related-merge-requests > li {
.recaptcha {
margin-bottom: 30px;
}
.new-branch-col {
padding-top: 10px;
}
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: flex;
}
.js-create-merge-request {
flex-grow: 1;
flex-shrink: 0;
}
.dropdown-menu {
width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
display: none;
}
.dropdown-toggle {
.fa-caret-down {
pointer-events: none;
margin-left: 0;
color: inherit;
margin-left: 0;
}
}
li:not(.divider) {
padding: 6px;
cursor: pointer;
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
}
.icon-container {
float: left;
padding-left: 6px;
i {
visibility: hidden;
}
}
.description {
padding-left: 30px;
font-size: 13px;
strong {
display: block;
font-weight: 600;
}
}
}
}
@media (min-width: $screen-sm-min) {
.new-branch-col {
padding-top: 0;
text-align: right;
}
.create-mr-dropdown-wrap {
.btn-group:not(.hide) {
display: inline-block;
}
}
}
......@@ -133,3 +133,55 @@
right: 160px;
}
}
.flex-project-members-panel {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@media (max-width: $screen-sm-min) {
display: block;
.flex-project-title {
vertical-align: top;
display: inline-block;
max-width: 90%;
}
}
.flex-project-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.badge {
height: 17px;
line-height: 16px;
margin-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
}
.flex-project-members-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
}
}
.panel {
.panel-heading {
.badge {
margin-top: 0;
}
@media (max-width: $screen-sm-min) {
.badge {
margin-right: 0;
margin-left: 0;
}
}
}
}
\ No newline at end of file
......@@ -511,7 +511,6 @@
.mr-version-controls {
background: $gray-light;
border-bottom: 1px solid $border-color;
color: $gl-text-color;
.mr-version-menus-container {
......
......@@ -67,7 +67,7 @@ ul.notes {
}
}
&.is-editting {
&.is-editing {
.note-header,
.note-text,
.edited-text {
......
......@@ -781,16 +781,11 @@
}
.scrollable-menu {
padding: 0;
max-height: 245px;
overflow: auto;
}
// Loading icon
.builds-dropdown-loading {
margin: 0 auto;
width: 20px;
}
// Action icon on the right
a.ci-action-icon-wrapper {
color: $action-icon-color;
......@@ -893,30 +888,29 @@
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu {
.arrow-up {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::before {
border-width: 0 5px 5px;
border-bottom-color: $border-color;
}
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::after {
margin-top: 1px;
border-bottom-color: $white-light;
}
&::before {
border-width: 0 5px 5px;
border-bottom-color: $border-color;
}
&::after {
margin-top: 1px;
border-bottom-color: $white-light;
}
}
......
module UploadsActions
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 unless uploader.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
end
......@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled
end
end
......@@ -16,7 +16,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def browse
directory = params[:path] ? "#{params[:path]}/" : ''
@path = params[:path]
directory = @path ? "#{@path}/" : ''
@entry = build.artifacts_metadata_entry(directory)
render_404 unless @entry.exists?
......@@ -60,7 +61,10 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def build
@build ||= build_from_id || build_from_ref
@build ||= begin
build = build_from_id || build_from_ref
build&.present(current_user: current_user)
end
end
def build_from_id
......
......@@ -46,20 +46,28 @@ class Projects::BranchesController < Projects::ApplicationController
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
if result[:status] == :success
@branch = result[:branch]
if redirect_to_autodeploy
redirect_to(
url_to_autodeploy_setup(project, branch_name),
notice: view_context.autodeploy_flash_notice(branch_name))
else
redirect_to namespace_project_tree_path(@project.namespace, @project,
@branch.name)
respond_to do |format|
format.html do
if result[:status] == :success
if redirect_to_autodeploy
redirect_to url_to_autodeploy_setup(project, branch_name),
notice: view_context.autodeploy_flash_notice(branch_name)
else
redirect_to namespace_project_tree_path(@project.namespace, @project, branch_name)
end
else
@error = result[:message]
render action: 'new'
end
end
format.json do
if result[:status] == :success
render json: { name: branch_name, url: namespace_project_tree_url(@project.namespace, @project, branch_name) }
else
render json: result[:messsage], status: :unprocessable_entity
end
end
else
@error = result[:message]
render action: 'new'
end
end
......
......@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch, :rendered_title]
:related_branches, :can_create_branch, :rendered_title, :create_merge_request]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show, :rendered_title]
......@@ -22,6 +22,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
respond_to :html
def index
......@@ -191,7 +194,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.json do
render json: { can_create_branch: can_create }
render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? }
end
end
end
......@@ -201,6 +204,16 @@ class Projects::IssuesController < Projects::ApplicationController
render json: { title: view_context.markdown_field(@issue, :title) }
end
def create_merge_request
result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
else
render json: result[:messsage], status: :unprocessable_entity
end
end
protected
def issue
......@@ -224,6 +237,10 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_issue, @project)
end
def authorize_create_merge_request!
return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
def module_enabled
return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end
......
......@@ -120,7 +120,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
define_diff_comment_vars
else
build_merge_request
@diffs = @merge_request.diffs(diff_options)
@compare = @merge_request
@diffs = @compare.diffs(diff_options)
@diff_notes_disabled = true
end
......@@ -584,12 +585,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
@diffs =
@compare =
if @start_sha
@merge_request_diff.compare_with(@start_sha).diffs(diff_options)
@merge_request_diff.compare_with(@start_sha)
else
@merge_request_diff.diffs(diff_options)
@merge_request_diff
end
@diffs = @compare.diffs(diff_options)
end
def define_diff_comment_vars
......@@ -598,11 +601,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
noteable_id: @merge_request.id
}
@diff_notes_disabled = !@merge_request_diff.latest? || @start_sha
@diff_notes_disabled = false
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@merge_request_diff.diff_refs)
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end
......
class Projects::PagesController < Projects::ApplicationController
layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_read_pages!, only: [:show]
before_action :authorize_update_pages!, except: [:show]
......
class Projects::PagesDomainsController < Projects::ApplicationController
layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy]
......
......@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
.new(project)
.execute(scope: @scope)
.new(project, scope: @scope)
.execute
.page(params[:page])
.per(30)
@running_count = PipelinesFinder
.new(project).execute(scope: 'running').count
.new(project, scope: 'running').execute.count
@pending_count = PipelinesFinder
.new(project).execute(scope: 'pending').count
.new(project, scope: 'pending').execute.count
@finished_count = PipelinesFinder
.new(project).execute(scope: 'finished').count
.new(project, scope: 'finished').execute.count
@pipelines_count = PipelinesFinder
.new(project).execute.count
......
......@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob?
if @blob.valid_lfs_pointer?
if @blob.stored_externally?
send_lfs_object
else
send_git_blob @repository, @blob
......
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 if uploader.nil? || !uploader.file.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition
end
private
def uploader
......@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController
def image_or_video?
uploader && uploader.file.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end
alias_method :model, :project
end
class UploadsController < ApplicationController
skip_before_action :authenticate_user!
before_action :find_model, :authorize_access!
def show
uploader = @model.send(upload_mount)
unless uploader.file_storage?
return redirect_to uploader.url
end
include UploadsActions
unless uploader.file && uploader.file.exists?
return render_404
end
disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
skip_before_action :authenticate_user!
before_action :find_model
before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
private
def find_model
unless upload_model && upload_mount
return render_404
end
return render_404 unless upload_model && upload_mount
@model = upload_model.find(params[:id])
end
def authorize_access!
authorized =
case @model
when Project
can?(current_user, :read_project, @model)
when Group
can?(current_user, :read_group, @model)
case model
when Note
can?(current_user, :read_project, @model.project)
else
# No authentication required for user avatars.
can?(current_user, :read_project, model.project)
when User
true
else
permission = "read_#{model.class.to_s.underscore}".to_sym
can?(current_user, permission, model)
end
return if authorized
render_unauthorized unless authorized
end
def authorize_create_access!
# for now we support only personal snippets comments
authorized = can?(current_user, :comment_personal_snippet, model)
render_unauthorized unless authorized
end
def render_unauthorized
if current_user
render_404
else
......@@ -58,17 +51,44 @@ class UploadsController < ApplicationController
"project" => Project,
"note" => Note,
"group" => Group,
"appearance" => Appearance
"appearance" => Appearance,
"personal_snippet" => PersonalSnippet
}
upload_models[params[:model]]
end
def upload_mount
return true unless params[:mounted_as]
upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as])
params[:mounted_as]
end
end
def uploader
return @uploader if defined?(@uploader)
if model.is_a?(PersonalSnippet)
@uploader = PersonalFileUploader.new(model, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
else
@uploader = @model.send(upload_mount)
redirect_to @uploader.url unless @uploader.file_storage?
end
@uploader
end
def uploader_class
PersonalFileUploader
end
def model
@model ||= find_model
end
end
class PipelinesFinder
attr_reader :project, :pipelines
attr_reader :project, :pipelines, :params
def initialize(project)
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
def initialize(project, params = {})
@project = project
@pipelines = project.pipelines
@params = params
end
def execute(scope: nil)
scoped_pipelines =
case scope
when 'running'
pipelines.running
when 'pending'
pipelines.pending
when 'finished'
pipelines.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
pipelines
end
scoped_pipelines.order(id: :desc)
def execute
items = pipelines
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
items = by_name(items)
items = by_username(items)
items = by_yaml_errors(items)
sort_items(items)
end
private
......@@ -43,4 +37,78 @@ class PipelinesFinder
def tags
project.repository.tag_names
end
def by_scope(items)
case params[:scope]
when 'running'
items.running
when 'pending'
items.pending
when 'finished'
items.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
items
end
end
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
def by_ref(items)
if params[:ref].present?
items.where(ref: params[:ref])
else
items
end
end
def by_name(items)
if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
else
items
end
end
def by_username(items)
if params[:username].present?
items.joins(:user).where(users: { username: params[:username] })
else
items
end
end
def by_yaml_errors(items)
case Gitlab::Utils.to_boolean(params[:yaml_errors])
when true
items.where("yaml_errors IS NOT NULL")
when false
items.where("yaml_errors IS NULL")
else
items
end
end
def sort_items(items)
order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by])
params[:order_by]
else
:id
end
sort = if params[:sort] =~ /\A(ASC|DESC)\z/i
params[:sort]
else
:desc
end
items.order(order_by => sort)
end
end
......@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.valid_lfs_pointer?
elsif blob.stored_externally?
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
......@@ -95,7 +95,7 @@ module BlobHelper
end
def can_modify_blob?(blob, project = @project, ref = @ref)
!blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
!blob.stored_externally? && can_edit_tree?(project, ref)
end
def leave_edit_message
......@@ -223,7 +223,9 @@ module BlobHelper
end
def open_raw_blob_button(blob)
if blob.raw_binary?
return if blob.empty?
if blob.raw_binary? || blob.stored_externally?
icon = icon('download')
title = 'Download'
else
......@@ -244,19 +246,27 @@ module BlobHelper
viewer.max_size
end
"it is larger than #{number_to_human_size(max_size)}"
when :server_side_but_stored_in_lfs
"it is stored in LFS"
when :server_side_but_stored_externally
case viewer.blob.external_storage
when :lfs
'it is stored in LFS'
else
'it is stored externally'
end
end
end
def blob_render_error_options(viewer)
error = viewer.render_error
options = []
if viewer.render_error == :too_large && viewer.can_override_max_size?
if error == :too_large && viewer.can_override_max_size?
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
end
if viewer.rich? && viewer.blob.rendered_as_text?
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
# so don't bother switching.
if viewer.rich? && viewer.blob.rendered_as_text? && error != :server_side_but_stored_externally
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end
......
......@@ -60,20 +60,16 @@ module NotesHelper
note.project.team.human_max_access(note.author_id)
end
def discussion_diff_path(discussion)
if discussion.for_merge_request? && discussion.diff_discussion?
if discussion.active?
# Without a diff ID, the link always points to the latest diff version
diff_id = nil
elsif merge_request_diff = discussion.latest_merge_request_diff
diff_id = merge_request_diff.id
else
# If the discussion is not active, and we cannot find the latest
# merge request diff for this discussion, we return no path at all.
return
end
diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, diff_id: diff_id, anchor: discussion.line_code)
def discussion_path(discussion)
if discussion.for_merge_request?
return unless discussion.diff_discussion?
version_params = discussion.merge_request_version_params
return unless version_params
path_params = version_params.merge(anchor: discussion.line_code)
diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, path_params)
elsif discussion.for_commit?
anchor = discussion.line_code if discussion.diff_discussion?
......
......@@ -70,6 +70,14 @@ module SortingHelper
}
end
def tags_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
end
def sort_title_priority
'Priority'
end
......
......@@ -76,7 +76,7 @@ module TreeHelper
"A new branch will be created in your fork and a new merge request will be started."
end
def tree_breadcrumbs(tree, max_links = 2)
def path_breadcrumbs(max_links = 6)
if @path.present?
part_path = ""
parts = @path.split('/')
......@@ -88,7 +88,7 @@ module TreeHelper
part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part)
yield(part, tree_join(@ref, part_path))
yield(part, part_path)
end
end
end
......
......@@ -28,7 +28,7 @@ class Blob < SimpleDelegator
BlobViewer::Sketch,
BlobViewer::Video,
BlobViewer::PDF,
BlobViewer::BinarySTL,
......@@ -75,19 +75,37 @@ class Blob < SimpleDelegator
end
def no_highlighting?
size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
def empty?
raw_size == 0
end
def too_large?
size && truncated?
end
def external_storage_error?
if external_storage == :lfs
!project&.lfs_enabled?
else
false
end
end
def stored_externally?
return @stored_externally if defined?(@stored_externally)
@stored_externally = external_storage && !external_storage_error?
end
# Returns the size of the file that this blob represents. If this blob is an
# LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
# the size of the blob itself.
def raw_size
if valid_lfs_pointer?
lfs_size
if stored_externally?
external_size
else
size
end
......@@ -98,9 +116,13 @@ class Blob < SimpleDelegator
# text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself.
def raw_binary?
if valid_lfs_pointer?
if stored_externally?
if rich_viewer
rich_viewer.binary?
elsif Linguist::Language.find_by_filename(name).any?
false
elsif _mime_type
_mime_type.binary?
else
true
end
......@@ -118,15 +140,7 @@ class Blob < SimpleDelegator
end
def readable_text?
text? && !valid_lfs_pointer? && !too_large?
end
def valid_lfs_pointer?
lfs_pointer? && project&.lfs_enabled?
end
def invalid_lfs_pointer?
lfs_pointer? && !project&.lfs_enabled?
text? && !stored_externally? && !too_large?
end
def simple_viewer
......@@ -165,10 +179,10 @@ class Blob < SimpleDelegator
end
def rich_viewer_class
return if invalid_lfs_pointer? || empty?
return if empty? || external_storage_error?
classes =
if valid_lfs_pointer?
if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS
elsif binary?
BINARY_VIEWERS
......
......@@ -70,12 +70,13 @@ module BlobViewer
return @render_error if defined?(@render_error)
@render_error =
if server_side_but_stored_in_lfs?
# Files stored in LFS can only be rendered using a client-side viewer,
if server_side_but_stored_externally?
# Files that are not stored in the repository, like LFS files and
# build artifacts, can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from
# `blob_raw_url` using AJAX.
:server_side_but_stored_in_lfs
:server_side_but_stored_externally
elsif override_max_size ? absolutely_too_large? : too_large?
:too_large
end
......@@ -89,8 +90,8 @@ module BlobViewer
private
def server_side_but_stored_in_lfs?
server_side? && blob.valid_lfs_pointer?
def server_side_but_stored_externally?
server_side? && blob.stored_externally?
end
end
end
......@@ -236,8 +236,8 @@ class Commit
project.pipelines.where(sha: sha)
end
def latest_pipeline
pipelines.last
def last_pipeline
@last_pipeline ||= pipelines.last
end
def status(ref = nil)
......
module BlobLike
extend ActiveSupport::Concern
include Linguist::BlobHelper
def id
raise NotImplementedError
end
def name
raise NotImplementedError
end
def path
raise NotImplementedError
end
def size
0
end
def data
nil
end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def truncated?
false
end
def external_storage
nil
end
def external_size
nil
end
end
......@@ -11,6 +11,7 @@ module DiscussionOnDiff
:diff_line,
:for_line?,
:active?,
:created_at_diff?,
to: :first_note
......
......@@ -30,6 +30,10 @@ module NoteOnDiff
raise NotImplementedError
end
def created_at_diff?(diff_refs)
false
end
private
def noteable_diff_refs
......
......@@ -10,7 +10,6 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
:latest_merge_request_diff,
to: :first_note
......@@ -18,6 +17,25 @@ class DiffDiscussion < Discussion
false
end
def merge_request_version_params
return unless for_merge_request?
if active?
{}
else
diff_refs = position.diff_refs
if diff = noteable.merge_request_diff_for(diff_refs)
{ diff_id: diff.id }
elsif diff = noteable.merge_request_diff_for(diff_refs.head_sha)
{
diff_id: diff.id,
start_sha: diff_refs.start_sha
}
end
end
end
def reply_attributes
super.merge(
original_position: original_position.to_json,
......
......@@ -65,10 +65,11 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
def latest_merge_request_diff
return unless for_merge_request?
def created_at_diff?(diff_refs)
return false unless supported?
return true if for_commit?
self.noteable.merge_request_diff_for(self.position.diff_refs)
self.original_position.diff_refs == diff_refs
end
private
......
......@@ -143,6 +143,14 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch
end
end
# To allow polymorphism with MergeRequest.
def source_project
project
......
......@@ -9,14 +9,14 @@ class LegacyDiffDiscussion < Discussion
memoized_values << :active
def legacy_diff_discussion?
true
end
def self.note_class
LegacyDiffNote
end
def legacy_diff_discussion?
true
end
def active?(*args)
return @active if @active.present?
......@@ -27,6 +27,16 @@ class LegacyDiffDiscussion < Discussion
!active?
end
def merge_request_version_params
return unless for_merge_request?
if active?
{}
else
nil
end
end
def reply_attributes
super.merge(line_code: line_code)
end
......
......@@ -374,12 +374,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true)
end
def merge_request_diff_for(diff_refs)
@merge_request_diffs_by_diff_refs ||= Hash.new do |h, diff_refs|
h[diff_refs] = merge_request_diffs.viewable.select_without_diff.find_by_diff_refs(diff_refs)
end
@merge_request_diffs_by_diff_refs[diff_refs]
def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
diffs = merge_request_diffs.viewable.select_without_diff
h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
diffs.find_by_diff_refs(diff_refs_or_sha)
else
diffs.find_by(head_commit_sha: diff_refs_or_sha)
end
end
@merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
end
def reload_diff_if_branch_changed
......
......@@ -115,11 +115,19 @@ class Note < ActiveRecord::Base
end
def grouped_diff_discussions(diff_refs = nil)
diff_notes.
fresh.
discussions.
select { |n| n.active?(diff_refs) }.
group_by(&:line_code)
groups = {}
diff_notes.fresh.discussions.each do |discussion|
if discussion.active?(diff_refs)
discussions = groups[discussion.line_code] ||= []
elsif diff_refs && discussion.created_at_diff?(diff_refs)
discussions = groups[discussion.original_line_code] ||= []
end
discussions << discussion if discussions
end
groups
end
def count_for_collection(ids, type)
......@@ -141,10 +149,6 @@ class Note < ActiveRecord::Base
true
end
def latest_merge_request_diff
nil
end
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
......
......@@ -50,5 +50,16 @@ module ChatMessage
def link(text, url)
"[#{text}](#{url})"
end
def pretty_duration(seconds)
parse_string =
if duration < 1.hour
'%M:%S'
else
'%H:%M:%S'
end
Time.at(seconds).utc.strftime(parse_string)
end
end
end
......@@ -15,7 +15,7 @@ module ChatMessage
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@duration = pipeline_attributes[:duration]
@duration = pipeline_attributes[:duration].to_i
@pipeline_id = pipeline_attributes[:id]
end
......@@ -37,7 +37,7 @@ module ChatMessage
{
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{duration} #{time_measure}",
text: "in #{pretty_duration(duration)}",
image: user_avatar || ''
}
end
......@@ -45,7 +45,7 @@ module ChatMessage
private
def message
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{time_measure}"
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
end
def humanized_status
......@@ -84,9 +84,5 @@ module ChatMessage
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
end
def time_measure
'second'.pluralize(duration)
end
end
end
......@@ -789,7 +789,7 @@ class Repository
}
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Rugged::Commit.create(rugged, options)
create_commit(options)
end
end
# rubocop:enable Metrics/ParameterLists
......@@ -836,7 +836,7 @@ class Repository
tree: merge_index.write_tree(rugged),
)
commit_id = Rugged::Commit.create(rugged, actual_options)
commit_id = create_commit(actual_options)
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
......@@ -859,12 +859,11 @@ class Repository
committer = user_to_committer(user)
Rugged::Commit.create(rugged,
message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [start_commit.sha])
create_commit(message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [start_commit.sha])
end
end
......@@ -883,16 +882,15 @@ class Repository
committer = user_to_committer(user)
Rugged::Commit.create(rugged,
message: commit.message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
create_commit(message: commit.message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
end
end
......@@ -900,7 +898,7 @@ class Repository
GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
create_commit(params.merge(author: committer, committer: committer))
end
end
......@@ -1142,6 +1140,12 @@ class Repository
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end
def create_commit(params = {})
params[:message].delete!("\r")
Rugged::Commit.create(rugged, params)
end
def repository_storage_path
@project.repository_storage_path
end
......
class SnippetBlob
include Linguist::BlobHelper
include BlobLike
attr_reader :snippet
......@@ -28,32 +28,4 @@ class SnippetBlob
Banzai.render_field(snippet, :content)
end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def lfs_pointer?
false
end
def lfs_oid
nil
end
def lfs_size
nil
end
def truncated?
false
end
end
......@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy
can! :read_personal_snippet if @subject.public?
return unless @user
if @subject.public?
can! :comment_personal_snippet
end
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet
can! :comment_personal_snippet
end
unless @user.external?
......@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.internal? && !@user.external?
can! :read_personal_snippet
can! :comment_personal_snippet
end
end
end
class MergeRequestCreateEntity < Grape::Entity
expose :iid
expose :url do |merge_request|
Gitlab::UrlBuilder.build(merge_request)
end
end
class MergeRequestCreateSerializer < BaseSerializer
entity MergeRequestCreateEntity
end
module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService
def execute
return error('Invalid issue iid') unless issue_iid.present? && issue.present?
result = CreateBranchService.new(project, current_user).execute(branch_name, ref)
return result if result[:status] == :error
SystemNoteService.new_issue_branch(issue, project, current_user, branch_name)
new_merge_request = create(merge_request)
if new_merge_request.valid?
success(new_merge_request)
else
error(new_merge_request.errors)
end
end
private
def issue_iid
@isssue_iid ||= params.delete(:issue_iid)
end
def issue
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
end
def branch_name
@branch_name ||= issue.to_branch_name
end
def ref
project.default_branch || 'master'
end
def merge_request
MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end
def merge_request_params
{
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id
}
end
def success(merge_request)
super().merge(merge_request: merge_request)
end
end
end
module Projects
class UploadService < BaseService
def initialize(project, file)
@project, @file = project, file
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
end
class UploadService
def initialize(model, file, uploader_class = FileUploader)
@model, @file, @uploader_class = model, file, uploader_class
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = @uploader_class.new(@model)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
......@@ -30,8 +30,4 @@ class ArtifactUploader < GitlabUploader
def filename
file.try(:filename)
end
def exists?
file.try(:exists?)
end
end
......@@ -26,11 +26,11 @@ class FileUploader < GitlabUploader
File.join(CarrierWave.root, base_dir, model.path_with_namespace)
end
attr_accessor :project
attr_accessor :model
attr_reader :secret
def initialize(project, secret = nil)
@project = project
def initialize(model, secret = nil)
@model = model
@secret = secret || generate_secret
end
......@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader
File.join(dynamic_path_segment, @secret)
end
def model
project
end
def relative_path
self.file.path.sub("#{dynamic_path_segment}/", '')
end
......
......@@ -33,4 +33,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
def relative_path
self.file.path.sub("#{root}/", '')
end
def exists?
file.try(:exists?)
end
end
......@@ -9,10 +9,6 @@ class LfsObjectUploader < GitlabUploader
"#{Gitlab.config.lfs.storage_path}/tmp/cache"
end
def exists?
file.try(:exists?)
end
def filename
model.oid[4..-1]
end
......
class PersonalFileUploader < FileUploader
def self.dynamic_path_segment(model)
File.join(CarrierWave.root, model_path(model))
end
private
def secure_url
File.join(self.class.model_path(model), secret, file.filename)
end
def self.model_path(model)
File.join("/#{base_dir}", model.class.to_s.underscore, model.id.to_s)
end
end
......@@ -3,7 +3,7 @@
.diff-file.file-holder
.js-file-title.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion)
.diff-content.code.js-syntax-highlight
%table
......
......@@ -20,7 +20,7 @@
= discussion.author.to_reference
started a discussion
- url = discussion_diff_path(discussion)
- url = discussion_path(discussion)
- if discussion.for_commit? && @noteable != discussion.noteable
on
- commit = discussion.noteable
......
......@@ -38,7 +38,7 @@
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments]) do
= nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
......
......@@ -11,7 +11,7 @@
- preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
- if current_user
:javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}";
- content_for :header_content do
......
......@@ -3,6 +3,6 @@
%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
%span.str-truncated
= link_to directory.name, path_to_directory
= link_to path_to_directory do
%span.str-truncated= directory.name
%td
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head"
.top-block.row-content-block.clearfix
.pull-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
= render "projects/builds/header", show_controls: false
.tree-holder
.nav-block
.tree-controls
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
%ul.breadcrumb.repo-breadcrumb
%li
= link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path|
%li
= link_to truncate(title, length: 40), browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path)
.tree-content-holder
%table.table.tree-table
%thead
......
......@@ -6,17 +6,14 @@
%li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
- tree_breadcrumbs(@tree, 6) do |title, path|
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
%li
- if path
- if path.end_with?(@path)
= link_to namespace_project_blob_path(@project.namespace, @project, path) do
%strong
= truncate(title, length: 40)
- else
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- if path == @path
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do
%strong= title
- else
= link_to title, '#'
= link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
......@@ -25,5 +22,4 @@
#blob-content-holder.blob-content-holder
%article.file-holder
= render "projects/blob/header", blob: blob
= render 'projects/blob/content', blob: blob
- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline
.content-block.build-header.top-area
.header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
Job
%strong.js-build-id ##{@build.id}
%strong
Job
= link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do
\##{@build.id}
in pipeline
= link_to pipeline_path(pipeline) do
%strong ##{pipeline.id}
......@@ -15,13 +18,16 @@
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
%code
= @build.ref
- if @build.user
= render "user"
= render "projects/builds/user" if @build.user
= time_ago_with_tooltip(@build.created_at)
.nav-controls
- 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'
- 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
%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')
- if show_controls
.nav-controls
- 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'
- 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
%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')
......@@ -61,19 +61,20 @@
%span.commit-info.branches
%i.fa.fa-spinner.fa-spin
- if @commit.status
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
.status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
= link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do
= ci_icon_for_status(@commit.status)
= link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do
= ci_icon_for_status(last_pipeline.status)
Pipeline
= link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace"
= ci_label_for_status(@commit.status)
- if @commit.latest_pipeline.stages.any?
= link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id), class: "monospace"
= ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages.any?
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: @commit.latest_pipeline, klass: 'js-commit-pipeline-graph'
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
in
= time_interval_in_words @commit.pipelines.total_duration
= time_interval_in_words last_pipeline.duration
:javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
......@@ -35,6 +35,6 @@
- else
= diff_line_content(line.text)
- if line_discussions
- if line_discussions&.any?
- discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?))
= render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded
- if can?(current_user, :push_code, @project)
.pull-right
#new-branch.new-branch{ 'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue) }
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
New branch
= link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do
= icon('exclamation-triangle')
New branch unavailable
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue), create_mr_path: create_merge_request_namespace_project_issue_path(@project.namespace, @project, @issue), create_branch_path: namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
.btn-group.available.hide
%input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: 'Create a merge request', data: { action: 'create-mr' } }
%button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
= icon('caret-down')
%ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
.menu-item
.icon-container
= icon('check')
.description
%strong Create a merge request
%span
Creates a branch named after this issue and a merge request. The source branch is '#{@project.default_branch}' by default.
%li.divider.droplab-item-ignore
%li{ role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
.menu-item
.icon-container
= icon('check')
.description
%strong Create a branch
%span
Creates a branch named after this issue. The source branch is '#{@project.default_branch}' by default.
......@@ -70,8 +70,11 @@
// This element is filled in using JavaScript.
.content-block.content-block-small
= render 'new_branch' unless @issue.confidential?
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.row
.col-sm-6
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.col-sm-6.new-branch-col
= render 'new_branch' unless @issue.confidential?
%section.issuable-discussion
= render 'projects/issues/discussion'
......
......@@ -35,7 +35,7 @@
%span.dropdown.inline.mr-version-compare-dropdown
%a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
%span
- if @start_sha
- if @start_version
version #{version_index(@start_version)}
- else
#{@merge_request.target_branch}
......@@ -59,7 +59,7 @@
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_version) do
%strong
#{@merge_request.target_branch} (base)
.monospace= short_sha(@merge_request_diff.base_commit_sha)
......@@ -75,13 +75,15 @@
= succeed '.' do
%code= @merge_request.target_branch
- if @diff_notes_disabled
- if @start_version || !@merge_request_diff.latest?
.comments-disabled-notif.content-block
= icon('info-circle')
- if @start_sha
Comments are disabled because you're comparing two versions of this merge request.
Not all comments are displayed because you're
- if @start_version
comparing two versions
- else
Discussions on this version of the merge request are displayed but comment creation is disabled.
viewing an old version
of this merge request.
.pull-right
= link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
......@@ -7,7 +7,7 @@
= render 'projects/notes/hints'
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-edit-warning
.settings-message.note-edit-warning.js-finish-edit-warning
Finish editing this message first!
= submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
......
.panel.panel-default
.nothing-here-block
GitLab Pages are disabled.
Ask your system's administrator to enable it.
......@@ -16,13 +16,10 @@
%hr.clearfix
- if Gitlab.config.pages.enabled
= render 'access'
= render 'use'
- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
= render 'list'
- else
= render 'no_domains'
= render 'destroy'
= render 'access'
= render 'use'
- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
= render 'list'
- else
= render 'disabled'
= render 'no_domains'
= render 'destroy'
......@@ -4,13 +4,13 @@
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- if project_nav_tab? :pipelines
= nav_link(path: 'pipelines#index', controller: :pipelines) do
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
- if project_nav_tab? :builds
= nav_link(controller: :builds) do
= nav_link(controller: [:builds, :artifacts]) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
......
......@@ -6,6 +6,12 @@
%p
Add a new member to
%strong= @project.name
- else
%p
Members can be added by project
%i Masters
or
%i Owners
.col-lg-9
.light.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
......
.panel.panel-default
.panel-heading
Members with access to
%strong= @project.name
.panel-heading.flex-project-members-panel
%span.flex-project-title
Members of
%strong
#{@project.name}
%span.badge= @project_members.total_count
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
......
......@@ -9,7 +9,7 @@
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Tag:
.col-md-10
.col-md-10.protected-tags-dropdown
= render partial: "projects/protected_tags/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
......
......@@ -27,7 +27,8 @@
= link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
= nav_link(controller: :pages) do
= link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
%span
Pages
- if Gitlab.config.pages.enabled
= nav_link(controller: :pages) do
= link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
%span
Pages
- @no_container = true
- @sort ||= sort_value_recently_updated
- page_title "Tags"
= render "projects/commits/head"
......@@ -14,16 +15,14 @@
.dropdown
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= projects_sort_options_hash[@sort]
= tags_sort_options_hash[@sort]
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_tags_path(sort: sort_value_name) do
= sort_title_name
= link_to filter_tags_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to filter_tags_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- tags_sort_options_hash.each do |value, title|
%li
= link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
- if can?(current_user, :push_code, @project)
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
New tag
......
......@@ -9,12 +9,9 @@
%li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
- tree_breadcrumbs(tree, 6) do |title, path|
- path_breadcrumbs do |title, path|
%li
- if path
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else
= link_to title, '#'
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
- if current_user
%li
......
......@@ -11,8 +11,8 @@
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
.arrow-up
.js-builds-dropdown-list.scrollable-menu
%li.js-builds-dropdown-list.scrollable-menu
.js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
%li.js-builds-dropdown-loading.hidden
.text-center
%i.fa.fa-spinner.fa-spin{ 'aria-hidden': 'true', 'aria-label': 'Loading' }
......@@ -2,7 +2,11 @@
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
%li.timeline-entry{ id: dom_id(note),
class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
data: { author_id: note.author.id,
editable: note_editable,
note_id: note.id } }
.timeline-entry-inner
.timeline-icon
- if note.system
......
---
title: Support uploaders for personal snippets comments
merge_request:
author:
---
title: Handle incoming emails from aliases correctly
merge_request:
author:
---
title: Improved UX on project members settings view
merge_request:
author:
---
title: 'API: Add parameters to allow filtering project pipelines'
merge_request: 9367
author: dosuken123
---
title: Allow to create new branch and empty WIP merge request from issue page
merge_request:
author:
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.
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