Commit d34f6764 authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge remote-tracking branch 'origin/master' into add-sentry-js-again-with-vue

parents 77bbc1c4 ef71bf62
...@@ -264,11 +264,15 @@ spinach mysql 9 10: *spinach-knapsack-mysql ...@@ -264,11 +264,15 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis: static-analysis:
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- scripts/static-analysis - 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" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -276,6 +280,7 @@ docs:check:links: ...@@ -276,6 +280,7 @@ docs:check:links:
dependencies: [] dependencies: []
before_script: [] before_script: []
script: script:
- scripts/lint-doc.sh
- mv doc/ /nanoc/content/ - mv doc/ /nanoc/content/
- cd /nanoc - cd /nanoc
# Build HTML from Markdown # Build HTML from Markdown
......
/* 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.'));
}
}
...@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() { ...@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form); new DropzoneInput(this.form);
autosize(this.textarea); autosize(this.textarea);
}
// form and textarea event listeners // form and textarea event listeners
this.addEventListeners(); this.addEventListeners();
}
gl.text.init(this.form); gl.text.init(this.form);
// hide discard button // hide discard button
this.form.find('.js-note-discard').hide(); 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 */ /* 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('./flash');
require('~/lib/utils/text_utility'); require('~/lib/utils/text_utility');
...@@ -18,48 +19,49 @@ class Issue { ...@@ -18,48 +19,49 @@ class Issue {
document.querySelector('#task_status_short').innerText = result.task_status_short; document.querySelector('#task_status_short').innerText = result.task_status_short;
} }
}); });
Issue.initIssueBtnEventListeners(); this.initIssueBtnEventListeners();
} }
Issue.$btnNewBranch = $('#new-branch'); Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
Issue.initMergeRequests(); Issue.initMergeRequests();
Issue.initRelatedBranches(); 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 issueFailMessage = 'Unable to update this issue at this time.';
const closeButtons = $('a.btn-close'); const closeButtons = $('a.btn-close');
const isClosedBadge = $('div.status-box-closed'); const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open'); const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter'); const projectIssuesCounter = $('.issue_counter');
const reopenButtons = $('a.btn-reopen'); const reopenButtons = $('a.btn-reopen');
return closeButtons.add(reopenButtons).on('click', function(e) { return closeButtons.add(reopenButtons).on('click', (e) => {
var $this, shouldSubmit, url; var $button, shouldSubmit, url;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(this); $button = $(e.currentTarget);
shouldSubmit = $this.hasClass('btn-comment'); shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) { if (shouldSubmit) {
Issue.submitNoteForm($this.closest('form')); Issue.submitNoteForm($button.closest('form'));
} }
$this.prop('disabled', true); $button.prop('disabled', true);
Issue.setNewBranchButtonState(true, null); url = $button.attr('href');
url = $this.attr('href');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
url: url url: url
}).fail(function(jqXHR, textStatus, errorThrown) { })
new Flash(issueFailMessage); .fail(() => new Flash(issueFailMessage))
Issue.initCanCreateBranch(); .done((data) => {
}).done(function(data, textStatus, jqXHR) {
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const isClosed = $this.hasClass('btn-close'); const isClosed = $button.hasClass('btn-close');
closeButtons.toggleClass('hidden', isClosed); closeButtons.toggleClass('hidden', isClosed);
reopenButtons.toggleClass('hidden', !isClosed); reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed); isClosedBadge.toggleClass('hidden', !isClosed);
...@@ -68,12 +70,21 @@ class Issue { ...@@ -68,12 +70,21 @@ class Issue {
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, '')); let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); 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 { } else {
new Flash(issueFailMessage); new Flash(issueFailMessage);
} }
$this.prop('disabled', false); $button.prop('disabled', false);
Issue.initCanCreateBranch();
}); });
}); });
} }
...@@ -109,29 +120,6 @@ class Issue { ...@@ -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; export default Issue;
...@@ -19,12 +19,10 @@ ...@@ -19,12 +19,10 @@
}); });
}; };
Milestone.sortIssues = function(data) { Milestone.sortIssues = function(url, data) {
var sort_issues_url;
sort_issues_url = location.href + "/sort_issues";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_issues_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -36,12 +34,10 @@ ...@@ -36,12 +34,10 @@
}); });
}; };
Milestone.sortMergeRequests = function(data) { Milestone.sortMergeRequests = function(url, data) {
var sort_mr_url;
sort_mr_url = location.href + "/sort_merge_requests";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_mr_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -81,42 +77,55 @@ ...@@ -81,42 +77,55 @@
}; };
function Milestone() { function Milestone() {
var oldMouseStart; this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint');
this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint');
this.bindIssuesSorting(); this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching(); this.bindTabsSwitching();
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab();
} }
Milestone.prototype.bindIssuesSorting = function() { Milestone.prototype.bindIssuesSorting = function() {
if (!this.issuesSortEndpoint) return;
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'issue-list', group: 'issue-list',
listEls: $('.issues-sortable-list'), listEls: $('.issues-sortable-list'),
fieldName: 'issue', fieldName: 'issue',
sortCallback: Milestone.sortIssues, sortCallback: (data) => {
Milestone.sortIssues(this.issuesSortEndpoint, data);
},
updateCallback: Milestone.updateIssue, updateCallback: Milestone.updateIssue,
}); });
}.bind(this)); }.bind(this));
}; };
Milestone.prototype.bindTabsSwitching = function() { Milestone.prototype.bindTabsSwitching = function() {
return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
var currentTabClass, previousTabClass; const $target = $(e.target);
currentTabClass = $(e.target).data('show');
previousTabClass = $(e.relatedTarget).data('show'); location.hash = $target.attr('href');
$(previousTabClass).hide(); this.loadTab($target);
$(currentTabClass).removeClass('hidden');
return $(currentTabClass).show();
}); });
}; };
Milestone.prototype.bindMergeRequestSorting = function() { Milestone.prototype.bindMergeRequestSorting = function() {
if (!this.mergeRequestsSortEndpoint) return;
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'merge-request-list', group: 'merge-request-list',
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
fieldName: 'merge_request', fieldName: 'merge_request',
sortCallback: Milestone.sortMergeRequests, sortCallback: (data) => {
Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data);
},
updateCallback: Milestone.updateMergeRequest, updateCallback: Milestone.updateMergeRequest,
}); });
}.bind(this)); }.bind(this));
...@@ -169,6 +178,35 @@ ...@@ -169,6 +178,35 @@
}); });
}; };
Milestone.prototype.loadInitialTab = function() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
if (tabElId === '#tab-merge-requests') {
this.bindMergeRequestSorting();
}
});
}
};
return Milestone; return Milestone;
})(); })();
}).call(window); }).call(window);
This diff is collapsed.
...@@ -425,12 +425,6 @@ ...@@ -425,12 +425,6 @@
float: right; float: right;
} }
.diffs {
.content-block {
border-bottom: none;
}
}
.files-changed { .files-changed {
border-bottom: none; border-bottom: none;
} }
......
...@@ -6,7 +6,13 @@ ...@@ -6,7 +6,13 @@
} }
.limit-container-width { .limit-container-width {
.detail-page-header { .detail-page-header,
.page-content-header,
.commit-box,
.info-well,
.notes,
.commit-ci-menu,
.files-changed {
@extend .fixed-width-container; @extend .fixed-width-container;
} }
...@@ -36,8 +42,7 @@ ...@@ -36,8 +42,7 @@
} }
.diffs { .diffs {
.mr-version-controls, .mr-version-controls {
.files-changed {
@extend .fixed-width-container; @extend .fixed-width-container;
} }
} }
......
...@@ -161,3 +161,86 @@ ul.related-merge-requests > li { ...@@ -161,3 +161,86 @@ ul.related-merge-requests > li {
.recaptcha { .recaptcha {
margin-bottom: 30px; 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 @@ ...@@ -133,3 +133,55 @@
right: 160px; 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 @@ ...@@ -511,7 +511,6 @@
.mr-version-controls { .mr-version-controls {
background: $gray-light; background: $gray-light;
border-bottom: 1px solid $border-color;
color: $gl-text-color; color: $gl-text-color;
.mr-version-menus-container { .mr-version-menus-container {
......
...@@ -67,7 +67,7 @@ ul.notes { ...@@ -67,7 +67,7 @@ ul.notes {
} }
} }
&.is-editting { &.is-editing {
.note-header, .note-header,
.note-text, .note-text,
.edited-text { .edited-text {
......
...@@ -71,7 +71,6 @@ ...@@ -71,7 +71,6 @@
.nav-controls { .nav-controls {
width: auto; width: auto;
min-width: 50%; min-width: 50%;
white-space: nowrap;
} }
} }
......
module MilestoneActions
extend ActiveSupport::Concern
def merge_requests
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
merge_requests: @milestone.merge_requests,
show_project_name: true
})
end
end
end
def participants
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
users: @milestone.participants
})
end
end
end
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
labels: @milestone.labels
})
end
end
end
private
def tabs_json(partial, data = {})
{
html: view_to_html_string(partial, data)
}
end
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
else
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
end
end
end
class Groups::MilestonesController < Groups::ApplicationController class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:show, :update] before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:new, :create, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index def index
......
...@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController ...@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user) return render_404 unless @project.feature_available?(:builds, current_user)
end end
def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled
end
end end
...@@ -16,7 +16,8 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -16,7 +16,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def browse def browse
directory = params[:path] ? "#{params[:path]}/" : '' @path = params[:path]
directory = @path ? "#{@path}/" : ''
@entry = build.artifacts_metadata_entry(directory) @entry = build.artifacts_metadata_entry(directory)
render_404 unless @entry.exists? render_404 unless @entry.exists?
...@@ -60,7 +61,10 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -60,7 +61,10 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def build 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 end
def build_from_id def build_from_id
......
...@@ -46,16 +46,14 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -46,16 +46,14 @@ class Projects::BranchesController < Projects::ApplicationController
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end end
respond_to do |format|
format.html do
if result[:status] == :success if result[:status] == :success
@branch = result[:branch]
if redirect_to_autodeploy if redirect_to_autodeploy
redirect_to( redirect_to url_to_autodeploy_setup(project, branch_name),
url_to_autodeploy_setup(project, branch_name), notice: view_context.autodeploy_flash_notice(branch_name)
notice: view_context.autodeploy_flash_notice(branch_name))
else else
redirect_to namespace_project_tree_path(@project.namespace, @project, redirect_to namespace_project_tree_path(@project.namespace, @project, branch_name)
@branch.name)
end end
else else
@error = result[:message] @error = result[:message]
...@@ -63,6 +61,16 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -63,6 +61,16 @@ class Projects::BranchesController < Projects::ApplicationController
end end
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
end
end
def destroy def destroy
@branch_name = Addressable::URI.unescape(params[:id]) @branch_name = Addressable::URI.unescape(params[:id])
status = DeleteBranchService.new(project, current_user).execute(@branch_name) status = DeleteBranchService.new(project, current_user).execute(@branch_name)
......
...@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, 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 # Allow read any issue
before_action :authorize_read_issue!, only: [:show, :rendered_title] before_action :authorize_read_issue!, only: [:show, :rendered_title]
...@@ -22,6 +22,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -22,6 +22,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update] 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 respond_to :html
def index def index
...@@ -191,7 +194,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -191,7 +194,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.json do 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 end
end end
...@@ -201,6 +204,16 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -201,6 +204,16 @@ class Projects::IssuesController < Projects::ApplicationController
render json: { title: view_context.markdown_field(@issue, :title) } render json: { title: view_context.markdown_field(@issue, :title) }
end 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 protected
def issue def issue
...@@ -224,6 +237,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -224,6 +237,10 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_issue, @project) return render_404 unless can?(current_user, :admin_issue, @project)
end 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 def module_enabled
return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end end
......
...@@ -120,7 +120,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -120,7 +120,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
define_diff_comment_vars define_diff_comment_vars
else else
build_merge_request build_merge_request
@diffs = @merge_request.diffs(diff_options) @compare = @merge_request
@diffs = @compare.diffs(diff_options)
@diff_notes_disabled = true @diff_notes_disabled = true
end end
...@@ -584,12 +585,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -584,12 +585,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
@diffs = @compare =
if @start_sha if @start_sha
@merge_request_diff.compare_with(@start_sha).diffs(diff_options) @merge_request_diff.compare_with(@start_sha)
else else
@merge_request_diff.diffs(diff_options) @merge_request_diff
end end
@diffs = @compare.diffs(diff_options)
end end
def define_diff_comment_vars def define_diff_comment_vars
...@@ -598,11 +601,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -598,11 +601,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
noteable_id: @merge_request.id 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? @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)) @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end end
......
class Projects::MilestonesController < Projects::ApplicationController class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :module_enabled before_action :module_enabled
before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels]
# Allow read any milestone # Allow read any milestone
before_action :authorize_read_milestone! before_action :authorize_read_milestone!
# Allow admin milestone # Allow admin milestone
before_action :authorize_admin_milestone!, except: [:index, :show] before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
respond_to :html respond_to :html
......
class Projects::PagesController < Projects::ApplicationController class Projects::PagesController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_read_pages!, only: [:show] before_action :authorize_read_pages!, only: [:show]
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
......
class Projects::PagesDomainsController < Projects::ApplicationController class Projects::PagesDomainsController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy] before_action :domain, only: [:show, :destroy]
......
...@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
.new(project) .new(project, scope: @scope)
.execute(scope: @scope) .execute
.page(params[:page]) .page(params[:page])
.per(30) .per(30)
@running_count = PipelinesFinder @running_count = PipelinesFinder
.new(project).execute(scope: 'running').count .new(project, scope: 'running').execute.count
@pending_count = PipelinesFinder @pending_count = PipelinesFinder
.new(project).execute(scope: 'pending').count .new(project, scope: 'pending').execute.count
@finished_count = PipelinesFinder @finished_count = PipelinesFinder
.new(project).execute(scope: 'finished').count .new(project, scope: 'finished').execute.count
@pipelines_count = PipelinesFinder @pipelines_count = PipelinesFinder
.new(project).execute.count .new(project).execute.count
......
...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob? return if cached_blob?
if @blob.valid_lfs_pointer? if @blob.stored_externally?
send_lfs_object send_lfs_object
else else
send_git_blob @repository, @blob send_git_blob @repository, @blob
......
class PipelinesFinder 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 @project = project
@pipelines = project.pipelines @pipelines = project.pipelines
@params = params
end
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
def ids_for_ref(refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
def from_ids(ids)
pipelines.unscoped.where(id: ids)
end
def branches
project.repository.branch_names
end
def tags
project.repository.tag_names
end end
def execute(scope: nil) def by_scope(items)
scoped_pipelines = case params[:scope]
case scope
when 'running' when 'running'
pipelines.running items.running
when 'pending' when 'pending'
pipelines.pending items.pending
when 'finished' when 'finished'
pipelines.finished items.finished
when 'branches' when 'branches'
from_ids(ids_for_ref(branches)) from_ids(ids_for_ref(branches))
when 'tags' when 'tags'
from_ids(ids_for_ref(tags)) from_ids(ids_for_ref(tags))
else else
pipelines items
end end
end
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
scoped_pipelines.order(id: :desc) items.where(status: params[:status])
end end
private def by_ref(items)
if params[:ref].present?
items.where(ref: params[:ref])
else
items
end
end
def ids_for_ref(refs) def by_name(items)
pipelines.where(ref: refs).group(:ref).select('max(id)') if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
else
items
end
end end
def from_ids(ids) def by_username(items)
pipelines.unscoped.where(id: ids) if params[:username].present?
items.joins(:user).where(users: { username: params[:username] })
else
items
end
end end
def branches def by_yaml_errors(items)
project.repository.branch_names 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 end
def tags def sort_items(items)
project.repository.tag_names 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
end end
...@@ -52,7 +52,7 @@ module BlobHelper ...@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref) 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' } 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' } 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) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
...@@ -95,7 +95,7 @@ module BlobHelper ...@@ -95,7 +95,7 @@ module BlobHelper
end end
def can_modify_blob?(blob, project = @project, ref = @ref) 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 end
def leave_edit_message def leave_edit_message
...@@ -223,7 +223,9 @@ module BlobHelper ...@@ -223,7 +223,9 @@ module BlobHelper
end end
def open_raw_blob_button(blob) def open_raw_blob_button(blob)
if blob.raw_binary? return if blob.empty?
if blob.raw_binary? || blob.stored_externally?
icon = icon('download') icon = icon('download')
title = 'Download' title = 'Download'
else else
...@@ -244,19 +246,27 @@ module BlobHelper ...@@ -244,19 +246,27 @@ module BlobHelper
viewer.max_size viewer.max_size
end end
"it is larger than #{number_to_human_size(max_size)}" "it is larger than #{number_to_human_size(max_size)}"
when :server_side_but_stored_in_lfs when :server_side_but_stored_externally
"it is stored in LFS" case viewer.blob.external_storage
when :lfs
'it is stored in LFS'
else
'it is stored externally'
end
end end
end end
def blob_render_error_options(viewer) def blob_render_error_options(viewer)
error = viewer.render_error
options = [] 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))) options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
end 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' }) options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end end
......
...@@ -115,4 +115,28 @@ module MilestonesHelper ...@@ -115,4 +115,28 @@ module MilestonesHelper
end end
end end
end end
def milestone_merge_request_tab_path(milestone)
if @project
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_participants_tab_path(milestone)
if @project
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_labels_tab_path(milestone)
if @project
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
end end
...@@ -60,20 +60,16 @@ module NotesHelper ...@@ -60,20 +60,16 @@ module NotesHelper
note.project.team.human_max_access(note.author_id) note.project.team.human_max_access(note.author_id)
end end
def discussion_diff_path(discussion) def discussion_path(discussion)
if discussion.for_merge_request? && discussion.diff_discussion? if discussion.for_merge_request?
if discussion.active? return unless discussion.diff_discussion?
# Without a diff ID, the link always points to the latest diff version
diff_id = nil version_params = discussion.merge_request_version_params
elsif merge_request_diff = discussion.latest_merge_request_diff return unless version_params
diff_id = merge_request_diff.id
else path_params = version_params.merge(anchor: discussion.line_code)
# 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) diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, path_params)
elsif discussion.for_commit? elsif discussion.for_commit?
anchor = discussion.line_code if discussion.diff_discussion? anchor = discussion.line_code if discussion.diff_discussion?
......
...@@ -76,7 +76,7 @@ module TreeHelper ...@@ -76,7 +76,7 @@ module TreeHelper
"A new branch will be created in your fork and a new merge request will be started." "A new branch will be created in your fork and a new merge request will be started."
end end
def tree_breadcrumbs(tree, max_links = 2) def path_breadcrumbs(max_links = 6)
if @path.present? if @path.present?
part_path = "" part_path = ""
parts = @path.split('/') parts = @path.split('/')
...@@ -88,7 +88,7 @@ module TreeHelper ...@@ -88,7 +88,7 @@ module TreeHelper
part_path = part if part_path.empty? part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part) 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 end
end end
......
...@@ -75,19 +75,37 @@ class Blob < SimpleDelegator ...@@ -75,19 +75,37 @@ class Blob < SimpleDelegator
end end
def no_highlighting? def no_highlighting?
size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
def empty?
raw_size == 0
end end
def too_large? def too_large?
size && truncated? size && truncated?
end 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 # 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 # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
# the size of the blob itself. # the size of the blob itself.
def raw_size def raw_size
if valid_lfs_pointer? if stored_externally?
lfs_size external_size
else else
size size
end end
...@@ -98,9 +116,13 @@ class Blob < SimpleDelegator ...@@ -98,9 +116,13 @@ class Blob < SimpleDelegator
# text-based rich blob viewer matched on the file's extension. Otherwise, this # text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself. # depends on the type of the blob itself.
def raw_binary? def raw_binary?
if valid_lfs_pointer? if stored_externally?
if rich_viewer if rich_viewer
rich_viewer.binary? rich_viewer.binary?
elsif Linguist::Language.find_by_filename(name).any?
false
elsif _mime_type
_mime_type.binary?
else else
true true
end end
...@@ -118,15 +140,7 @@ class Blob < SimpleDelegator ...@@ -118,15 +140,7 @@ class Blob < SimpleDelegator
end end
def readable_text? def readable_text?
text? && !valid_lfs_pointer? && !too_large? text? && !stored_externally? && !too_large?
end
def valid_lfs_pointer?
lfs_pointer? && project&.lfs_enabled?
end
def invalid_lfs_pointer?
lfs_pointer? && !project&.lfs_enabled?
end end
def simple_viewer def simple_viewer
...@@ -165,10 +179,10 @@ class Blob < SimpleDelegator ...@@ -165,10 +179,10 @@ class Blob < SimpleDelegator
end end
def rich_viewer_class def rich_viewer_class
return if invalid_lfs_pointer? || empty? return if empty? || external_storage_error?
classes = classes =
if valid_lfs_pointer? if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS BINARY_VIEWERS + TEXT_VIEWERS
elsif binary? elsif binary?
BINARY_VIEWERS BINARY_VIEWERS
......
...@@ -70,12 +70,13 @@ module BlobViewer ...@@ -70,12 +70,13 @@ module BlobViewer
return @render_error if defined?(@render_error) return @render_error if defined?(@render_error)
@render_error = @render_error =
if server_side_but_stored_in_lfs? if server_side_but_stored_externally?
# Files stored in LFS can only be rendered using a client-side viewer, # 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 # 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 # server side. Client-side viewers use JS and can fetch the file from
# `blob_raw_url` using AJAX. # `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? elsif override_max_size ? absolutely_too_large? : too_large?
:too_large :too_large
end end
...@@ -89,8 +90,8 @@ module BlobViewer ...@@ -89,8 +90,8 @@ module BlobViewer
private private
def server_side_but_stored_in_lfs? def server_side_but_stored_externally?
server_side? && blob.valid_lfs_pointer? server_side? && blob.stored_externally?
end end
end end
end end
...@@ -236,8 +236,8 @@ class Commit ...@@ -236,8 +236,8 @@ class Commit
project.pipelines.where(sha: sha) project.pipelines.where(sha: sha)
end end
def latest_pipeline def last_pipeline
pipelines.last @last_pipeline ||= pipelines.last
end end
def status(ref = nil) 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 ...@@ -11,6 +11,7 @@ module DiscussionOnDiff
:diff_line, :diff_line,
:for_line?, :for_line?,
:active?, :active?,
:created_at_diff?,
to: :first_note to: :first_note
......
...@@ -30,6 +30,10 @@ module NoteOnDiff ...@@ -30,6 +30,10 @@ module NoteOnDiff
raise NotImplementedError raise NotImplementedError
end end
def created_at_diff?(diff_refs)
false
end
private private
def noteable_diff_refs def noteable_diff_refs
......
...@@ -10,7 +10,6 @@ class DiffDiscussion < Discussion ...@@ -10,7 +10,6 @@ class DiffDiscussion < Discussion
delegate :position, delegate :position,
:original_position, :original_position,
:latest_merge_request_diff,
to: :first_note to: :first_note
...@@ -18,6 +17,25 @@ class DiffDiscussion < Discussion ...@@ -18,6 +17,25 @@ class DiffDiscussion < Discussion
false false
end 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 def reply_attributes
super.merge( super.merge(
original_position: original_position.to_json, original_position: original_position.to_json,
......
...@@ -65,10 +65,11 @@ class DiffNote < Note ...@@ -65,10 +65,11 @@ class DiffNote < Note
self.position.diff_refs == diff_refs self.position.diff_refs == diff_refs
end end
def latest_merge_request_diff def created_at_diff?(diff_refs)
return unless for_merge_request? 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 end
private private
......
...@@ -143,6 +143,14 @@ class Issue < ActiveRecord::Base ...@@ -143,6 +143,14 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request branches_with_iid - branches_with_merge_request
end 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. # To allow polymorphism with MergeRequest.
def source_project def source_project
project project
......
...@@ -9,14 +9,14 @@ class LegacyDiffDiscussion < Discussion ...@@ -9,14 +9,14 @@ class LegacyDiffDiscussion < Discussion
memoized_values << :active memoized_values << :active
def legacy_diff_discussion?
true
end
def self.note_class def self.note_class
LegacyDiffNote LegacyDiffNote
end end
def legacy_diff_discussion?
true
end
def active?(*args) def active?(*args)
return @active if @active.present? return @active if @active.present?
...@@ -27,6 +27,16 @@ class LegacyDiffDiscussion < Discussion ...@@ -27,6 +27,16 @@ class LegacyDiffDiscussion < Discussion
!active? !active?
end end
def merge_request_version_params
return unless for_merge_request?
if active?
{}
else
nil
end
end
def reply_attributes def reply_attributes
super.merge(line_code: line_code) super.merge(line_code: line_code)
end end
......
...@@ -374,12 +374,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -374,12 +374,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true) merge_request_diff(true)
end end
def merge_request_diff_for(diff_refs) def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs ||= Hash.new do |h, diff_refs| @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
h[diff_refs] = merge_request_diffs.viewable.select_without_diff.find_by_diff_refs(diff_refs) 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 end
@merge_request_diffs_by_diff_refs[diff_refs] @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
end end
def reload_diff_if_branch_changed def reload_diff_if_branch_changed
......
...@@ -115,11 +115,19 @@ class Note < ActiveRecord::Base ...@@ -115,11 +115,19 @@ class Note < ActiveRecord::Base
end end
def grouped_diff_discussions(diff_refs = nil) def grouped_diff_discussions(diff_refs = nil)
diff_notes. groups = {}
fresh.
discussions. diff_notes.fresh.discussions.each do |discussion|
select { |n| n.active?(diff_refs) }. if discussion.active?(diff_refs)
group_by(&:line_code) 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 end
def count_for_collection(ids, type) def count_for_collection(ids, type)
...@@ -141,10 +149,6 @@ class Note < ActiveRecord::Base ...@@ -141,10 +149,6 @@ class Note < ActiveRecord::Base
true true
end end
def latest_merge_request_diff
nil
end
def max_attachment_size def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i current_application_settings.max_attachment_size.megabytes.to_i
end end
......
...@@ -50,5 +50,16 @@ module ChatMessage ...@@ -50,5 +50,16 @@ module ChatMessage
def link(text, url) def link(text, url)
"[#{text}](#{url})" "[#{text}](#{url})"
end 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
end end
...@@ -15,7 +15,7 @@ module ChatMessage ...@@ -15,7 +15,7 @@ module ChatMessage
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref] @ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status] @status = pipeline_attributes[:status]
@duration = pipeline_attributes[:duration] @duration = pipeline_attributes[:duration].to_i
@pipeline_id = pipeline_attributes[:id] @pipeline_id = pipeline_attributes[:id]
end end
...@@ -37,7 +37,7 @@ module ChatMessage ...@@ -37,7 +37,7 @@ module ChatMessage
{ {
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}", title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: "in #{duration} #{time_measure}", text: "in #{pretty_duration(duration)}",
image: user_avatar || '' image: user_avatar || ''
} }
end end
...@@ -45,7 +45,7 @@ module ChatMessage ...@@ -45,7 +45,7 @@ module ChatMessage
private private
def message 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 end
def humanized_status def humanized_status
...@@ -84,9 +84,5 @@ module ChatMessage ...@@ -84,9 +84,5 @@ module ChatMessage
def pipeline_link def pipeline_link
"[##{pipeline_id}](#{pipeline_url})" "[##{pipeline_id}](#{pipeline_url})"
end end
def time_measure
'second'.pluralize(duration)
end
end end
end end
...@@ -789,7 +789,7 @@ class Repository ...@@ -789,7 +789,7 @@ class Repository
} }
options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Rugged::Commit.create(rugged, options) create_commit(options)
end end
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
...@@ -836,7 +836,7 @@ class Repository ...@@ -836,7 +836,7 @@ class Repository
tree: merge_index.write_tree(rugged), 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) merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id commit_id
end end
...@@ -859,8 +859,7 @@ class Repository ...@@ -859,8 +859,7 @@ class Repository
committer = user_to_committer(user) committer = user_to_committer(user)
Rugged::Commit.create(rugged, create_commit(message: commit.revert_message(user),
message: commit.revert_message(user),
author: committer, author: committer,
committer: committer, committer: committer,
tree: revert_tree_id, tree: revert_tree_id,
...@@ -883,8 +882,7 @@ class Repository ...@@ -883,8 +882,7 @@ class Repository
committer = user_to_committer(user) committer = user_to_committer(user)
Rugged::Commit.create(rugged, create_commit(message: commit.message,
message: commit.message,
author: { author: {
email: commit.author_email, email: commit.author_email,
name: commit.author_name, name: commit.author_name,
...@@ -900,7 +898,7 @@ class Repository ...@@ -900,7 +898,7 @@ class Repository
GitOperationService.new(user, self).with_branch(branch_name) do GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user) 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
end end
...@@ -1142,6 +1140,12 @@ class Repository ...@@ -1142,6 +1140,12 @@ class Repository
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end end
def create_commit(params = {})
params[:message].delete!("\r")
Rugged::Commit.create(rugged, params)
end
def repository_storage_path def repository_storage_path
@project.repository_storage_path @project.repository_storage_path
end end
......
class SnippetBlob class SnippetBlob
include Linguist::BlobHelper include BlobLike
attr_reader :snippet attr_reader :snippet
...@@ -28,32 +28,4 @@ class SnippetBlob ...@@ -28,32 +28,4 @@ class SnippetBlob
Banzai.render_field(snippet, :content) Banzai.render_field(snippet, :content)
end 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 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
...@@ -4,7 +4,10 @@ module Projects ...@@ -4,7 +4,10 @@ module Projects
key = accessible_keys.find_by(id: params[:key_id] || params[:id]) key = accessible_keys.find_by(id: params[:key_id] || params[:id])
return unless key return unless key
unless project.deploy_keys.include?(key)
project.deploy_keys << key project.deploy_keys << key
end
key key
end end
......
...@@ -175,11 +175,7 @@ ...@@ -175,11 +175,7 @@
.panel-body .panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
%ul = render 'users/deletion_guidance', user: @user
%li All user content like authored issues, snippets, comments will be removed
- rp = @user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
%br %br
= link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.diff-file.file-holder .diff-file.file-holder
.js-file-title.file-title .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 .diff-content.code.js-syntax-highlight
%table %table
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= discussion.author.to_reference = discussion.author.to_reference
started a discussion started a discussion
- url = discussion_diff_path(discussion) - url = discussion_path(discussion)
- if discussion.for_commit? && @noteable != discussion.noteable - if discussion.for_commit? && @noteable != discussion.noteable
on on
- commit = discussion.noteable - commit = discussion.noteable
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :pipelines - 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 = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span %span
Pipelines Pipelines
......
...@@ -118,11 +118,7 @@ ...@@ -118,11 +118,7 @@
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p %p
Deleting an account has the following effects: Deleting an account has the following effects:
%ul = render 'users/deletion_guidance', user: current_user
%li All user content like authored issues, snippets, comments will be removed
- rp = current_user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
......
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
%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
= tree_icon('folder', '755', directory.name) = tree_icon('folder', '755', directory.name)
%span.str-truncated = link_to path_to_directory do
= link_to directory.name, path_to_directory %span.str-truncated= directory.name
%td %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 = render "projects/builds/header", show_controls: false
.pull-right
.tree-holder
.nav-block
.tree-controls
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), = link_to download_namespace_project_build_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
.tree-holder %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 .tree-content-holder
%table.table.tree-table %table.table.tree-table
%thead %thead
......
...@@ -6,17 +6,14 @@ ...@@ -6,17 +6,14 @@
%li %li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path = @project.path
- tree_breadcrumbs(@tree, 6) do |title, path| - path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
%li %li
- if path - if path == @path
- if path.end_with?(@path) = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do
= link_to namespace_project_blob_path(@project.namespace, @project, path) do %strong= title
%strong
= truncate(title, length: 40)
- else - else
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
- else
= link_to title, '#'
%ul.blob-commit-info.hidden-xs %ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
...@@ -25,5 +22,4 @@ ...@@ -25,5 +22,4 @@
#blob-content-holder.blob-content-holder #blob-content-holder.blob-content-holder
%article.file-holder %article.file-holder
= render "projects/blob/header", blob: blob = render "projects/blob/header", blob: blob
= render 'projects/blob/content', blob: blob = render 'projects/blob/content', blob: blob
- blame = local_assigns.fetch(:blame, false) - blame = local_assigns.fetch(:blame, false)
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
.file-header-content = render 'projects/blob/header_content', blob: blob
= blob_icon blob.mode, blob.name
%strong.file-title-name
= blob.name
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
.file-actions.hidden-xs .file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob unless blame = render 'projects/blob/viewer_switcher', blob: blob unless blame
......
.file-header-content
= blob_icon blob.mode, blob.name
%strong.file-title-name
= blob.name
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline - pipeline = @build.pipeline
.content-block.build-header.top-area .content-block.build-header.top-area
.header-content .header-content
= 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
Job Job
%strong.js-build-id ##{@build.id} = link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do
\##{@build.id}
in pipeline in pipeline
= link_to pipeline_path(pipeline) do = link_to pipeline_path(pipeline) do
%strong ##{pipeline.id} %strong ##{pipeline.id}
...@@ -15,9 +18,12 @@ ...@@ -15,9 +18,12 @@
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
%code %code
= @build.ref = @build.ref
- if @build.user
= render "user" = render "projects/builds/user" if @build.user
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
- if show_controls
.nav-controls .nav-controls
- 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'
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
- 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:
= link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request) = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold'
- if @build.duration - if @build.duration
%p.build-detail-row %p.build-detail-row
%span.build-light-text Duration: %span.build-light-text Duration:
......
...@@ -61,19 +61,20 @@ ...@@ -61,19 +61,20 @@
%span.commit-info.branches %span.commit-info.branches
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
- if @commit.status - if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info .well-segment.pipeline-info
.status-icon-container{ class: "ci-status-icon-#{@commit.status}" } .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
= link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do = link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do
= ci_icon_for_status(@commit.status) = ci_icon_for_status(last_pipeline.status)
Pipeline Pipeline
= link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace" = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id), class: "monospace"
= ci_label_for_status(@commit.status) = ci_label_for_status(last_pipeline.status)
- if @commit.latest_pipeline.stages.any? - if last_pipeline.stages.any?
.mr-widget-pipeline-graph .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 in
= time_interval_in_words @commit.pipelines.total_duration = time_interval_in_words last_pipeline.duration
:javascript :javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
- @no_container = true - @no_container = true
- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : ''
- limited_container_width = fluid_layout || diff_view == :inline ? '' : 'limit-container-width'
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description - page_description @commit.description
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: container_class } .container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box" = render "commit_box"
- if @commit.status - if @commit.status
= render "ci_menu" = render "ci_menu"
......
...@@ -35,6 +35,6 @@ ...@@ -35,6 +35,6 @@
- else - else
= diff_line_content(line.text) = diff_line_content(line.text)
- if line_discussions - if line_discussions&.any?
- discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?)) - discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?))
= render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded = render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
.pull-right .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) } }
#new-branch.new-branch{ 'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue) } .btn-group.unavailable
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do = icon('spinner', class: 'fa-spin')
New branch %span.text
= link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do Checking branch availability…
= icon('exclamation-triangle') .btn-group.available.hide
New branch unavailable %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 @@ ...@@ -70,8 +70,11 @@
// This element is filled in using JavaScript. // This element is filled in using JavaScript.
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch' unless @issue.confidential? .row
.col-sm-6
= render 'award_emoji/awards_block', awardable: @issue, inline: true = render 'award_emoji/awards_block', awardable: @issue, inline: true
.col-sm-6.new-branch-col
= render 'new_branch' unless @issue.confidential?
%section.issuable-discussion %section.issuable-discussion
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
%span.dropdown.inline.mr-version-compare-dropdown %span.dropdown.inline.mr-version-compare-dropdown
%a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} } %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
%span %span
- if @start_sha - if @start_version
version #{version_index(@start_version)} version #{version_index(@start_version)}
- else - else
#{@merge_request.target_branch} #{@merge_request.target_branch}
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
%small %small
= time_ago_with_tooltip(merge_request_diff.created_at) = time_ago_with_tooltip(merge_request_diff.created_at)
%li %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 %strong
#{@merge_request.target_branch} (base) #{@merge_request.target_branch} (base)
.monospace= short_sha(@merge_request_diff.base_commit_sha) .monospace= short_sha(@merge_request_diff.base_commit_sha)
...@@ -75,13 +75,15 @@ ...@@ -75,13 +75,15 @@
= succeed '.' do = succeed '.' do
%code= @merge_request.target_branch %code= @merge_request.target_branch
- if @diff_notes_disabled - if @start_version || !@merge_request_diff.latest?
.comments-disabled-notif.content-block .comments-disabled-notif.content-block
= icon('info-circle') = icon('info-circle')
- if @start_sha Not all comments are displayed because you're
Comments are disabled because you're comparing two versions of this merge request. - if @start_version
comparing two versions
- else - 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 .pull-right
= link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm' = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= render 'projects/notes/hints' = render 'projects/notes/hints'
.note-form-actions.clearfix .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! Finish editing this message first!
= submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-button' = submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: '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 @@ ...@@ -16,13 +16,10 @@
%hr.clearfix %hr.clearfix
- if Gitlab.config.pages.enabled = render 'access'
= render 'access' = render 'use'
= render 'use' - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
= render 'list' = render 'list'
- else
= render 'no_domains'
= render 'destroy'
- else - else
= render 'disabled' = render 'no_domains'
= render 'destroy'
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
.nav-links.sub-nav.scrolling-tabs .nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) } %ul{ class: (container_class) }
- if project_nav_tab? :pipelines - 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 = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span %span
Pipelines Pipelines
- if project_nav_tab? :builds - 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 = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span %span
Jobs Jobs
......
...@@ -6,6 +6,12 @@ ...@@ -6,6 +6,12 @@
%p %p
Add a new member to Add a new member to
%strong= @project.name %strong= @project.name
- else
%p
Members can be added by project
%i Masters
or
%i Owners
.col-lg-9 .col-lg-9
.light.prepend-top-default .light.prepend-top-default
- if can?(current_user, :admin_project_member, @project) - if can?(current_user, :admin_project_member, @project)
......
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading.flex-project-members-panel
Members with access to %span.flex-project-title
%strong= @project.name Members of
%strong
#{@project.name}
%span.badge= @project_members.total_count %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 .form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } = 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" } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.form-group .form-group
= f.label :name, class: 'col-md-2 text-right' do = f.label :name, class: 'col-md-2 text-right' do
Tag: Tag:
.col-md-10 .col-md-10.protected-tags-dropdown
= render partial: "projects/protected_tags/dropdown", locals: { f: f } = render partial: "projects/protected_tags/dropdown", locals: { f: f }
.help-block .help-block
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
= link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span %span
CI/CD Pipelines CI/CD Pipelines
- if Gitlab.config.pages.enabled
= nav_link(controller: :pages) do = nav_link(controller: :pages) do
= link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
%span %span
......
...@@ -9,12 +9,9 @@ ...@@ -9,12 +9,9 @@
%li %li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path = @project.path
- tree_breadcrumbs(tree, 6) do |title, path| - path_breadcrumbs do |title, path|
%li %li
- if path = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else
= link_to title, '#'
- if current_user - if current_user
%li %li
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
- selected_labels.each do |label| - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels") = multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true') = icon('chevron-down', 'aria-hidden': 'true')
......
.text-center.prepend-top-default
= icon('spin spinner 2x', 'aria-hidden': 'true', 'aria-label': 'Loading tab content')
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs.js-milestone-tabs
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues Issues
%span.badge= milestone.issues_visible_to_user(current_user).size %span.badge= milestone.issues_visible_to_user(current_user).size
%li %li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests Merge Requests
%span.badge= milestone.merge_requests.size %span.badge= milestone.merge_requests.size
- else - else
%li.active %li.active
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests Merge Requests
%span.badge= milestone.merge_requests.size %span.badge= milestone.merge_requests.size
%li %li
= link_to '#tab-participants', 'data-toggle' => 'tab' do = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants Participants
%span.badge= milestone.participants.count %span.badge= milestone.participants.count
%li %li
= link_to '#tab-labels', 'data-toggle' => 'tab' do = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels Labels
%span.badge= milestone.labels.count %span.badge= milestone.labels.count
...@@ -30,14 +30,18 @@ ...@@ -30,14 +30,18 @@
.tab-content.milestone-content .tab-content.milestone-content
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
.tab-pane.active#tab-issues .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests .tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name -# loaded async
= render "shared/milestones/tab_loading"
- else - else
.tab-pane.active#tab-merge-requests .tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants .tab-pane#tab-participants
= render 'shared/milestones/participants_tab', users: milestone.participants -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-labels .tab-pane#tab-labels
= render 'shared/milestones/labels_tab', labels: milestone.labels -# loaded async
= render "shared/milestones/tab_loading"
...@@ -2,7 +2,11 @@ ...@@ -2,7 +2,11 @@
- return if note.cross_reference_not_visible_for?(current_user) - return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note) - 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-entry-inner
.timeline-icon .timeline-icon
- if note.system - if note.system
......
- blob = @snippet.blob - blob = @snippet.blob
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
.file-header-content = render 'projects/blob/header_content', blob: blob
= blob_icon blob.mode, blob.path
%strong.file-title-name
= blob.path
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
.file-actions.hidden-xs .file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob = render 'projects/blob/viewer_switcher', blob: blob
......
- user = local_assigns.fetch(:user)
%ul
%li
%p
Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the
= link_to 'user account deletion documentation.', help_page_path("user/profile/account/delete_account", anchor: "associated-records")
- personal_projects_count = user.personal_projects.count
- unless personal_projects_count.zero?
%li #{pluralize(personal_projects_count, 'personal project')} will be removed and cannot be restored
---
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:
---
title: Detect already enabled DeployKeys in EnableDeployKeyService
merge_request:
author:
---
title: Update note edits in real-time
merge_request:
author:
---
title: Disable navigation to Project-level pages configuration when Pages disabled
merge_request: 11008
author:
---
title: Fix label creation from issuable for subgroup projects
merge_request:
author:
---
title: Fix error on CI/CD Settings page related to invalid pipeline trigger
merge_request: 10948
author: dosuken123
---
title: Note Ghost user and refer to user deletion documentation
merge_request:
author:
---
title: Expose project statistics on single requests via the API
merge_request:
author:
---
title: Fix caching large snippet HTML content on MySQL databases
merge_request: 11024
author:
---
title: Remove carriage returns from commit messages
merge_request: 11077
author:
---
title: Fix misaligned buttons in wiki pages
merge_request: 11043
author:
---
title: Always show the latest pipeline information in the commit box
merge_request: 11038
author:
---
title: Load milestone tabs asynchronously to increase initial load performance
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.
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