Commit 6a92dff2 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into sh-headless-chrome-support

* master: (96 commits)
  Fetch the merged branches at once
  Merging EE doc into CE
  Avoid using Rugged in Gitlab::Git::Wiki#preview_slug
  Cache commits on the repository model
  Remove groups_select from global namespace & simplifies the code
  Change default disabled merge request widget message to "Merge is not allowed yet"
  Semi-linear history merge is now available in CE.
  Remove repetitive karma spec
  Improve spec to check hidden component
  Rename to shouldShowUsername
  Add KubernetesService#default_namespace tests
  Revert "Merge branch '36670-remove-edit-form' into 'master'"
  Fix bitbucket login
  Remove duped tests
  Add path attribute to WikiFile class
  Make local_branches OPT_OUT
  Clarify the language around External Group membership with SAML SSO to clarify that this will NOT add users to GitLab Groups.
  Added ssh fingerprint, gitlab ci and pages information in an instance configuration page
  Fix the incorrect value being used to set GL_USERNAME on hooks
  Resolve "Remove overzealous tooltips in projects page tabs"
  ...
parents 40ee89bc 1aae91cf
...@@ -100,7 +100,7 @@ entry. ...@@ -100,7 +100,7 @@ entry.
- [CHANGED] Added defaults for protected branches dropdowns on the repository settings. !14278 - [CHANGED] Added defaults for protected branches dropdowns on the repository settings. !14278
- [CHANGED] Show confirmation modal before deleting account. !14360 - [CHANGED] Show confirmation modal before deleting account. !14360
- [CHANGED] Allow creating merge requests across a fork network. !14422 - [CHANGED] Allow creating merge requests across a fork network. !14422
- [CHANGED] Re-arrange <script> tags before <template> tags in .vue files. !14671 - [CHANGED] Re-arrange script HTML tags before template HTML tags in .vue files. !14671
- [CHANGED] Create idea of read-only database. !14688 - [CHANGED] Create idea of read-only database. !14688
- [CHANGED] Add active states to nav bar counters. - [CHANGED] Add active states to nav bar counters.
- [CHANGED] Add view replaced file link for image diffs. - [CHANGED] Add view replaced file link for image diffs.
......
...@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels) - [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging) - [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals) - [Feature proposals](#feature-proposals)
......
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.45.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.48.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -274,7 +274,7 @@ GEM ...@@ -274,7 +274,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.45.0) gitaly-proto (0.48.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1026,7 +1026,7 @@ DEPENDENCIES ...@@ -1026,7 +1026,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.45.0) gitaly-proto (~> 0.48.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
...@@ -25,6 +25,11 @@ ...@@ -25,6 +25,11 @@
type: String, type: String,
required: true, required: true,
}, },
viewType: {
type: String,
required: false,
default: 'child',
},
}, },
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
...@@ -110,6 +115,7 @@ ...@@ -110,6 +115,7 @@
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/> />
</div> </div>
</div> </div>
......
...@@ -2,7 +2,7 @@ import Cookies from 'js-cookie'; ...@@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
import _ from 'underscore'; import _ from 'underscore';
import bp from './breakpoints'; import bp from './breakpoints';
export default class NewNavSidebar { export default class ContextualSidebar {
constructor() { constructor() {
this.initDomElements(); this.initDomElements();
this.render(); this.render();
...@@ -55,7 +55,7 @@ export default class NewNavSidebar { ...@@ -55,7 +55,7 @@ export default class NewNavSidebar {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed); this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
} }
NewNavSidebar.setCollapsedCookie(collapsed); ContextualSidebar.setCollapsedCookie(collapsed);
this.toggleSidebarOverflow(); this.toggleSidebarOverflow();
} }
......
/* eslint-disable class-methods-use-this */
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff'; import SingleFileDiff from './single_file_diff';
...@@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index'; ...@@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
class Diff { export default class Diff {
constructor() { constructor() {
const $diffFile = $('.files .diff-file'); const $diffFile = $('.files .diff-file');
...@@ -104,7 +102,7 @@ class Diff { ...@@ -104,7 +102,7 @@ class Diff {
} }
this.highlightSelectedLine(); this.highlightSelectedLine();
} }
// eslint-disable-next-line class-methods-use-this
handleParallelLineDown(e) { handleParallelLineDown(e) {
const line = $(e.currentTarget); const line = $(e.currentTarget);
const table = line.closest('table'); const table = line.closest('table');
...@@ -116,11 +114,11 @@ class Diff { ...@@ -116,11 +114,11 @@ class Diff {
table.addClass(`${lineClass}-selected`); table.addClass(`${lineClass}-selected`);
} }
} }
// eslint-disable-next-line class-methods-use-this
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
} }
// eslint-disable-next-line class-methods-use-this
lineNumbers(line) { lineNumbers(line) {
const children = line.find('.diff-line-num').toArray(); const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) { if (children.length !== 2) {
...@@ -128,7 +126,7 @@ class Diff { ...@@ -128,7 +126,7 @@ class Diff {
} }
return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0); return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
} }
// eslint-disable-next-line class-methods-use-this
highlightSelectedLine() { highlightSelectedLine() {
const hash = gl.utils.getLocationHash(); const hash = gl.utils.getLocationHash();
const $diffFiles = $('.diff-file'); const $diffFiles = $('.diff-file');
...@@ -141,6 +139,3 @@ class Diff { ...@@ -141,6 +139,3 @@ class Diff {
} }
} }
} }
window.gl = window.gl || {};
window.gl.Diff = Diff;
...@@ -8,11 +8,12 @@ ...@@ -8,11 +8,12 @@
/* global NewBranchForm */ /* global NewBranchForm */
/* global NotificationsForm */ /* global NotificationsForm */
/* global NotificationsDropdown */ /* global NotificationsDropdown */
/* global GroupAvatar */ import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */ /* global LineHighlighter */
import BuildArtifacts from './build_artifacts'; import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor'; import CILintEditor from './ci_lint_editor';
/* global GroupsSelect */ import groupsSelect from './groups_select';
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
/* global NamespaceSelects */ /* global NamespaceSelects */
...@@ -87,6 +88,7 @@ import U2FAuthenticate from './u2f/authenticate'; ...@@ -87,6 +88,7 @@ import U2FAuthenticate from './u2f/authenticate';
import Members from './members'; import Members from './members';
import memberExpirationDate from './member_expiration_date'; import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select'; import DueDateSelectors from './due_date_select';
import Diff from './diff';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -237,8 +239,9 @@ import DueDateSelectors from './due_date_select'; ...@@ -237,8 +239,9 @@ import DueDateSelectors from './due_date_select';
new GLForm($('.milestone-form'), true); new GLForm($('.milestone-form'), true);
break; break;
case 'projects:compare:show': case 'projects:compare:show':
new gl.Diff(); new Diff();
initChangesDropdown(); const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
break; break;
case 'projects:branches:new': case 'projects:branches:new':
case 'projects:branches:create': case 'projects:branches:create':
...@@ -273,7 +276,7 @@ import DueDateSelectors from './due_date_select'; ...@@ -273,7 +276,7 @@ import DueDateSelectors from './due_date_select';
} }
case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new gl.Diff(); new Diff();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true); new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
...@@ -307,7 +310,7 @@ import DueDateSelectors from './due_date_select'; ...@@ -307,7 +310,7 @@ import DueDateSelectors from './due_date_select';
new GLForm($('.release-form'), true); new GLForm($('.release-form'), true);
break; break;
case 'projects:merge_requests:show': case 'projects:merge_requests:show':
new gl.Diff(); new Diff();
shortcut_handler = new ShortcutsIssuable(true); shortcut_handler = new ShortcutsIssuable(true);
new ZenMode(); new ZenMode();
...@@ -323,7 +326,7 @@ import DueDateSelectors from './due_date_select'; ...@@ -323,7 +326,7 @@ import DueDateSelectors from './due_date_select';
new gl.Activities(); new gl.Activities();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
new gl.Diff(); new Diff();
new ZenMode(); new ZenMode();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({ new MiniPipelineGraph({
...@@ -411,7 +414,7 @@ import DueDateSelectors from './due_date_select'; ...@@ -411,7 +414,7 @@ import DueDateSelectors from './due_date_select';
break; break;
case 'projects:project_members:index': case 'projects:project_members:index':
memberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); groupsSelect();
memberExpirationDate(); memberExpirationDate();
new Members(); new Members();
new UsersSelect(); new UsersSelect();
...@@ -422,11 +425,11 @@ import DueDateSelectors from './due_date_select'; ...@@ -422,11 +425,11 @@ import DueDateSelectors from './due_date_select';
case 'admin:groups:create': case 'admin:groups:create':
BindInOut.initAll(); BindInOut.initAll();
new Group(); new Group();
new GroupAvatar(); groupAvatar();
break; break;
case 'groups:edit': case 'groups:edit':
case 'admin:groups:edit': case 'admin:groups:edit':
new GroupAvatar(); groupAvatar();
break; break;
case 'projects:tree:show': case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -473,7 +476,7 @@ import DueDateSelectors from './due_date_select'; ...@@ -473,7 +476,7 @@ import DueDateSelectors from './due_date_select';
const $el = $(el); const $el = $(el);
if ($el.find('.dropdown-group-label').length) { if ($el.find('.dropdown-group-label').length) {
new gl.GroupLabelSubscription($el); new GroupLabelSubscription($el);
} else { } else {
new gl.ProjectLabelSubscription($el); new gl.ProjectLabelSubscription($el);
} }
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global notes */
/* Developer beware! Do not add logic to showButton or hideButton /* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance * that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what * bottleneck for pages with large diffs. For a comprehensive list of what
...@@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded'; ...@@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded';
export default { export default {
init($diffFile) { init($diffFile) {
/* Caching is used only when the following members are *true*. This is because there are likely to be /* Caching is used only when the following members are *true*.
* differently configured versions of diffs in the same session. However if these values are true, they * This is because there are likely to be
* differently configured versions of diffs in the same session.
* However if these values are true, they
* will be true in all cases */ * will be true in all cases */
if (!this.userCanCreateNote) { if (!this.userCanCreateNote) {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ export default function groupAvatar() {
$('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() {
window.GroupAvatar = (function() { const form = $(this).closest('form');
function GroupAvatar() { return form.find('.js-group-avatar-input').click();
$('.js-choose-group-avatar-button').on("click", function() { });
var form; $('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
form = $(this).closest("form"); const form = $(this).closest('form');
return form.find(".js-group-avatar-input").click(); // eslint-disable-next-line no-useless-escape
}); const filename = $(this).val().replace(/^.*[\\\/]/, '');
$('.js-group-avatar-input').on("change", function() { return form.find('.js-avatar-filename').text(filename);
var filename, form; });
form = $(this).closest("form"); }
filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find(".js-avatar-filename").text(filename);
});
}
return GroupAvatar;
})();
/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ export default class GroupLabelSubscription {
class GroupLabelSubscription {
constructor(container) { constructor(container) {
const $container = $(container); const $container = $(container);
this.$dropdown = $container.find('.dropdown'); this.$dropdown = $container.find('.dropdown');
...@@ -18,7 +16,7 @@ class GroupLabelSubscription { ...@@ -18,7 +16,7 @@ class GroupLabelSubscription {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url url,
}).done(() => { }).done(() => {
this.toggleSubscriptionButtons(); this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url'); this.$unsubscribeButtons.removeAttr('data-url');
...@@ -35,7 +33,7 @@ class GroupLabelSubscription { ...@@ -35,7 +33,7 @@ class GroupLabelSubscription {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url url,
}).done(() => { }).done(() => {
this.toggleSubscriptionButtons(); this.toggleSubscriptionButtons();
}); });
...@@ -47,6 +45,3 @@ class GroupLabelSubscription { ...@@ -47,6 +45,3 @@ class GroupLabelSubscription {
this.$unsubscribeButtons.toggleClass('hidden'); this.$unsubscribeButtons.toggleClass('hidden');
} }
} }
window.gl = window.gl || {};
window.gl.GroupLabelSubscription = GroupLabelSubscription;
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
camelcase, one-var-declaration-per-line, quotes, object-shorthand,
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
import Api from './api'; import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils'; import { normalizeCRLFHeaders } from './lib/utils/common_utils';
var slice = [].slice; export default function groupsSelect() {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('all-available');
const skipGroups = $select.data('skip-groups') || [];
$select.select2({
placeholder: 'Search for a group',
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
return $.ajax(params)
.then((data, status, xhr) => {
const results = data || [];
window.GroupsSelect = (function() { const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
function GroupsSelect() { const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
$('.ajax-groups-select').each((function(_this) { const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const self = _this; const more = currentPage < totalPages;
return function(i, select) {
var all_available, skip_groups;
const $select = $(select);
all_available = $select.data('all-available');
skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group",
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport: function (params) {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
return { return {
results, results,
page, pagination: {
more, more,
},
}; };
}, })
}, .then(params.success)
initSelection: function(element, callback) { .fail(params.error);
var id; },
id = $(element).val(); data(search, page) {
if (id !== "") { return {
return Api.group(id, callback); search,
} page,
}, per_page: window.GROUP_SELECT_PER_PAGE,
formatResult: function() { all_available: allAvailable,
var args; };
args = 1 <= arguments.length ? slice.call(arguments, 0) : []; },
return self.formatResult.apply(self, args); results(data, page) {
}, if (data.length) return { results: [] };
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatSelection.apply(self, args);
},
dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
GroupsSelect.prototype.formatResult = function(group) {
var avatar;
if (group.avatar_url) {
avatar = group.avatar_url;
} else {
avatar = gon.default_avatar_url;
}
return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
};
GroupsSelect.prototype.formatSelection = function(group) {
return group.full_name;
};
GroupsSelect.prototype.forceOverflow = function (e) { const groups = data.length ? data : data.results || [];
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; const more = data.pagination ? data.pagination.more : false;
}; const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
GroupsSelect.PER_PAGE = 20; return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
return GroupsSelect; $select.on('select2-loaded', () => {
})(); const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
}
import stickyMonitor from './lib/utils/sticky'; import stickyMonitor from './lib/utils/sticky';
export default () => { export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed')); stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({ $('.js-diff-stats-dropdown').glDropdown({
filterable: true, filterable: true,
......
...@@ -51,20 +51,19 @@ const PARTICIPANTS_ROW_COUNT = 7; ...@@ -51,20 +51,19 @@ const PARTICIPANTS_ROW_COUNT = 7;
} }
IssuableContext.prototype.initParticipants = function() { IssuableContext.prototype.initParticipants = function() {
$(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
return $(".js-participants-author").each(function(i) { return $('.js-participants-author').each(function(i) {
if (i >= PARTICIPANTS_ROW_COUNT) { if (i >= PARTICIPANTS_ROW_COUNT) {
return $(this).addClass("js-participants-hidden").hide(); return $(this).addClass('js-participants-hidden').hide();
} }
}); });
}; };
IssuableContext.prototype.toggleHiddenParticipants = function(e) { IssuableContext.prototype.toggleHiddenParticipants = function() {
var currentText, lessText, originalText; const currentText = $(this).text().trim();
e.preventDefault(); const lessText = $(this).data('less-text');
currentText = $(this).text().trim(); const originalText = $(this).data('original-text');
lessText = $(this).data("less-text");
originalText = $(this).data("original-text");
if (currentText === originalText) { if (currentText === originalText) {
$(this).text(lessText); $(this).text(lessText);
...@@ -73,7 +72,7 @@ const PARTICIPANTS_ROW_COUNT = 7; ...@@ -73,7 +72,7 @@ const PARTICIPANTS_ROW_COUNT = 7;
$(this).text(originalText); $(this).text(originalText);
} }
$(".js-participants-hidden").toggle(); $('.js-participants-hidden').toggle();
}; };
return IssuableContext; return IssuableContext;
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import NewNavSidebar from './new_sidebar'; import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav'; import initFlyOutNav from './fly_out_nav';
(function() { (function() {
...@@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav'; ...@@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav';
}); });
$(() => { $(() => {
const newNavSidebar = new NewNavSidebar(); const contextualSidebar = new ContextualSidebar();
newNavSidebar.bindEvents(); contextualSidebar.bindEvents();
initFlyOutNav(); initFlyOutNav();
}); });
......
...@@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { ...@@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
} }
}; };
export default (el, insertPlaceholder = true) => { export default (el, stickyTop, insertPlaceholder = true) => {
if (!el) return; if (!el) return;
const computedStyle = window.getComputedStyle(el); if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
if (!/sticky/.test(computedStyle.position)) return;
const stickyTop = parseInt(computedStyle.top, 10);
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true, passive: true,
......
...@@ -50,16 +50,11 @@ import './compare_autocomplete'; ...@@ -50,16 +50,11 @@ import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import './copy_as_gfm'; import './copy_as_gfm';
import './copy_to_clipboard'; import './copy_to_clipboard';
import './diff';
import './files_comment_button';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
import './gl_field_error'; import './gl_field_error';
import './gl_field_errors'; import './gl_field_errors';
import './gl_form'; import './gl_form';
import './group_avatar';
import './group_label_subscription';
import './groups_select';
import './header'; import './header';
import './importer_status'; import './importer_status';
import './issuable_index'; import './issuable_index';
......
...@@ -11,8 +11,8 @@ import { ...@@ -11,8 +11,8 @@ import {
handleLocationHash, handleLocationHash,
isMetaClick, isMetaClick,
} from './lib/utils/common_utils'; } from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab'; import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
...@@ -67,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab'; ...@@ -67,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
class MergeRequestTabs { class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const paddingTop = 16;
this.diffsLoaded = false; this.diffsLoaded = false;
this.pipelinesLoaded = false; this.pipelinesLoaded = false;
this.commitsLoaded = false; this.commitsLoaded = false;
...@@ -76,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab'; ...@@ -76,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
this.setCurrentAction = this.setCurrentAction.bind(this); this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this); this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this); this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
if (stubLocation) { if (stubLocation) {
location = stubLocation; location = stubLocation;
...@@ -278,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab'; ...@@ -278,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
const $container = $('#diffs'); const $container = $('#diffs');
$container.html(data.html); $container.html(data.html);
initChangesDropdown(); initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
...@@ -292,7 +301,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab'; ...@@ -292,7 +301,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
} }
this.diffsLoaded = true; this.diffsLoaded = true;
new gl.Diff(); new Diff();
this.scrollToElement('#diffs'); this.scrollToElement('#diffs');
$('.diff-file').each((i, el) => { $('.diff-file').each((i, el) => {
......
...@@ -1280,10 +1280,12 @@ export default class Notes { ...@@ -1280,10 +1280,12 @@ export default class Notes {
* Get data from Form attributes to use for saving/submitting comment. * Get data from Form attributes to use for saving/submitting comment.
*/ */
getFormData($form) { getFormData($form) {
const content = $form.find('.js-note-text').val();
return { return {
formData: $form.serialize(), formData: $form.serialize(),
formContent: _.escape($form.find('.js-note-text').val()), formContent: _.escape(content),
formAction: $form.attr('action'), formAction: $form.attr('action'),
formContentOriginal: content,
}; };
} }
...@@ -1415,7 +1417,7 @@ export default class Notes { ...@@ -1415,7 +1417,7 @@ export default class Notes {
const isMainForm = $form.hasClass('js-main-target-form'); const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction } = this.getFormData($form); const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId; let noteUniqueId;
let systemNoteUniqueId; let systemNoteUniqueId;
let hasQuickActions = false; let hasQuickActions = false;
...@@ -1574,7 +1576,7 @@ export default class Notes { ...@@ -1574,7 +1576,7 @@ export default class Notes {
$form = $notesContainer.parent().find('form'); $form = $notesContainer.parent().find('form');
} }
$form.find('.js-note-text').val(formContent); $form.find('.js-note-text').val(formContentOriginal);
this.reenableTargetFormSubmitButton(e); this.reenableTargetFormSubmitButton(e);
this.addNoteError($form); this.addNoteError($form);
}); });
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue'; import issueNoteForm from './issue_note_form.vue';
import placeholderNote from './issue_placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
import * as constants from '../constants'; import * as constants from '../constants';
import issueNote from './issue_note.vue'; import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue'; import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import issueCommentForm from './issue_comment_form.vue'; import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
components: { components: {
issueNote, issueNote,
issueDiscussion, issueDiscussion,
issueSystemNote, systemNote,
issueCommentForm, issueCommentForm,
loadingIcon, loadingIcon,
placeholderNote, placeholderNote,
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
} }
return placeholderNote; return placeholderNote;
} else if (note.individual_note) { } else if (note.individual_note) {
return note.notes[0].system ? issueSystemNote : issueNote; return note.notes[0].system ? systemNote : issueNote;
} }
return issueDiscussion; return issueDiscussion;
......
...@@ -12,6 +12,15 @@ ...@@ -12,6 +12,15 @@
type: Object, type: Object,
required: true, required: true,
}, },
// Can be rendered in 3 different places, with some visual differences
// Accepts root | child
// `root` -> main view
// `child` -> rendered inside MR or Commit View
viewType: {
type: String,
required: false,
default: 'root',
},
}, },
components: { components: {
tablePagination, tablePagination,
...@@ -187,7 +196,7 @@ ...@@ -187,7 +196,7 @@
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
/> />
<error-state <error-state
v-if="shouldRenderErrorState" v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath" :error-state-svg-path="errorStateSvgPath"
/> />
...@@ -206,6 +215,7 @@ ...@@ -206,6 +215,7 @@
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath" :auto-devops-help-path="autoDevopsPath"
:view-type="viewType"
/> />
</div> </div>
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
type: String, type: String,
required: true, required: true,
}, },
viewType: {
type: String,
required: true,
},
}, },
components: { components: {
pipelinesTableRowComponent, pipelinesTableRowComponent,
...@@ -59,6 +63,7 @@ ...@@ -59,6 +63,7 @@
:pipeline="model" :pipeline="model"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/> />
</div> </div>
</template> </template>
...@@ -29,6 +29,10 @@ export default { ...@@ -29,6 +29,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
viewType: {
type: String,
required: true,
},
}, },
components: { components: {
asyncButtonComponent, asyncButtonComponent,
...@@ -203,9 +207,13 @@ export default { ...@@ -203,9 +207,13 @@ export default {
displayPipelineActions() { displayPipelineActions() {
return this.pipeline.flags.retryable || return this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable || this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length || this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length; this.pipeline.details.artifacts.length;
},
isChildView() {
return this.viewType === 'child';
}, },
}, },
}; };
...@@ -218,7 +226,10 @@ export default { ...@@ -218,7 +226,10 @@ export default {
Status Status
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<ci-badge :status="pipelineStatus"/> <ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
/>
</div> </div>
</div> </div>
...@@ -240,7 +251,9 @@ export default { ...@@ -240,7 +251,9 @@ export default {
:commit-url="commitUrl" :commit-url="commitUrl"
:short-sha="commitShortSha" :short-sha="commitShortSha"
:title="commitTitle" :title="commitTitle"
:author="commitAuthor"/> :author="commitAuthor"
:show-branch="!isChildView"
/>
</div> </div>
</div> </div>
......
<script>
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
export default {
components: {
loadingIcon,
},
props: {
currentBranch: {
type: String,
required: true,
},
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
submitNewBranch() {
// need to query as the element is appended outside of Vue
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
this.loading = true;
if (flashEl) {
hideFlash(flashEl, false);
}
eventHub.$emit('createNewBranch', this.branchName);
},
showErrorMessage(message) {
this.loading = false;
flash(message, 'alert', this.$el);
},
createdNewBranch(newBranchName) {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = newBranchName;
}
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
eventHub.$on('createNewBranchError', this.showErrorMessage);
eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
},
destroyed() {
eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
eventHub.$off('createNewBranchError', this.showErrorMessage);
},
};
</script>
<template>
<div>
<div
class="flash-container"
ref="flashContainer"
>
</div>
<p>
Create from:
<code>{{ currentBranch }}</code>
</p>
<input
class="form-control js-new-branch-name"
type="text"
placeholder="Name new branch"
v-model="branchName"
@keyup.enter.stop.prevent="submitNewBranch"
/>
<div class="prepend-top-default clearfix">
<button
type="button"
class="btn btn-primary pull-left"
:disabled="btnDisabled"
@click.stop.prevent="submitNewBranch"
>
<loading-icon
v-if="loading"
:inline="true"
/>
<span>Create</span>
</button>
<button
type="button"
class="btn btn-default pull-right"
@click.stop.prevent="toggleDropdown"
>
Cancel
</button>
</div>
</div>
</template>
<script>
import RepoStore from '../../stores/repo_store';
import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub';
import newModal from './modal.vue';
export default {
components: {
newModal,
},
data() {
return {
openModal: false,
modalType: '',
currentPath: RepoStore.path,
};
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
createNewEntryInStore(name, type) {
RepoHelper.createNewEntry(name, type);
this.toggleModalOpen();
},
},
created() {
eventHub.$on('createNewEntry', this.createNewEntryInStore);
},
beforeDestroy() {
eventHub.$off('createNewEntry', this.createNewEntryInStore);
},
};
</script>
<template>
<div>
<ul class="breadcrumb repo-breadcrumb">
<li class="dropdown">
<button
type="button"
class="btn btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<i
class="fa fa-plus"
aria-hidden="true"
>
</i>
</button>
<ul class="dropdown-menu">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal
v-if="openModal"
:type="modalType"
:current-path="currentPath"
@toggle="toggleModalOpen"
/>
</div>
</template>
<script>
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import eventHub from '../../event_hub';
export default {
props: {
currentPath: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.currentPath !== '' ? `${this.currentPath}/` : '',
};
},
components: {
popupDialog,
},
methods: {
createEntryInStore() {
eventHub.$emit('createNewEntry', this.entryName, this.type);
},
toggleModalOpen() {
this.$emit('toggle');
},
},
computed: {
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
},
};
</script>
<template>
<popup-dialog
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@toggle="toggleModalOpen"
@submit="createEntryInStore"
>
<form
class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
</form>
</popup-dialog>
</template>
...@@ -8,7 +8,9 @@ import RepoMixin from '../mixins/repo_mixin'; ...@@ -8,7 +8,9 @@ import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import eventHub from '../event_hub';
export default { export default {
data() { data() {
...@@ -24,12 +26,19 @@ export default { ...@@ -24,12 +26,19 @@ export default {
PopupDialog, PopupDialog,
RepoPreview, RepoPreview,
}, },
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() { mounted() {
Helper.getContent().catch(Helper.loadingError); Helper.getContent().catch(Helper.loadingError);
}, },
destroyed() {
eventHub.$off('createNewBranch', this.createNewBranch);
},
methods: { methods: {
getCurrentLocation() {
return location.href;
},
toggleDialogOpen(toggle) { toggleDialogOpen(toggle) {
this.dialog.open = toggle; this.dialog.open = toggle;
}, },
...@@ -37,9 +46,30 @@ export default { ...@@ -37,9 +46,30 @@ export default {
dialogSubmitted(status) { dialogSubmitted(status) {
this.toggleDialogOpen(false); this.toggleDialogOpen(false);
this.dialog.status = status; this.dialog.status = status;
},
// remove tmp files
Helper.removeAllTmpFiles('openedFiles');
Helper.removeAllTmpFiles('files');
},
toggleBlobView: Store.toggleBlobView, toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) {
Service.createBranch({
branch,
ref: Store.currentBranch,
}).then((res) => {
const newBranchName = res.data.name;
const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
Store.currentBranch = newBranchName;
history.pushState({ key: Helper.key }, '', newUrl);
eventHub.$emit('createNewBranchSuccess', newBranchName);
eventHub.$emit('toggleNewBranchDropdown');
}).catch((err) => {
eventHub.$emit('createNewBranchError', err.response.data.message);
});
},
}, },
}; };
</script> </script>
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
action: 'update', action: f.tempFile ? 'create' : 'update',
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
...@@ -62,7 +62,6 @@ export default { ...@@ -62,7 +62,6 @@ export default {
if (newBranch) { if (newBranch) {
payload.start_branch = this.currentBranch; payload.start_branch = this.currentBranch;
} }
this.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(() => { .then(() => {
this.resetCommitState(); this.resetCommitState();
...@@ -78,6 +77,8 @@ export default { ...@@ -78,6 +77,8 @@ export default {
}, },
tryCommit(e, skipBranchCheck = false, newBranch = false) { tryCommit(e, skipBranchCheck = false, newBranch = false) {
this.submitCommitsLoading = true;
if (skipBranchCheck) { if (skipBranchCheck) {
this.makeCommit(newBranch); this.makeCommit(newBranch);
} else { } else {
...@@ -90,6 +91,7 @@ export default { ...@@ -90,6 +91,7 @@ export default {
this.makeCommit(newBranch); this.makeCommit(newBranch);
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false;
Flash('An error occurred while committing your changes'); Flash('An error occurred while committing your changes');
}); });
} }
......
...@@ -16,7 +16,7 @@ const RepoEditor = { ...@@ -16,7 +16,7 @@ const RepoEditor = {
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile.raw_path) Service.getRaw(this.activeFile)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data; Store.activeFile.plain = rawResponse.data;
......
...@@ -11,7 +11,12 @@ const RepoFileButtons = { ...@@ -11,7 +11,12 @@ const RepoFileButtons = {
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
showButtons() {
return this.activeFile.raw_path ||
this.activeFile.blame_path ||
this.activeFile.commits_path ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() { rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw'; return this.binary ? 'Download' : 'Raw';
}, },
...@@ -30,7 +35,10 @@ export default RepoFileButtons; ...@@ -30,7 +35,10 @@ export default RepoFileButtons;
</script> </script>
<template> <template>
<div id="repo-file-buttons"> <div
v-if="showButtons"
class="repo-file-buttons"
>
<a <a
:href="activeFile.raw_path" :href="activeFile.raw_path"
target="_blank" target="_blank"
......
...@@ -18,8 +18,8 @@ const RepoTab = { ...@@ -18,8 +18,8 @@ const RepoTab = {
}, },
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times close-icon': !this.tab.changed, 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
'fa-circle unsaved-icon': this.tab.changed, 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
}; };
return tabChangedObj; return tabChangedObj;
}, },
...@@ -30,7 +30,7 @@ const RepoTab = { ...@@ -30,7 +30,7 @@ const RepoTab = {
Store.setActiveFiles(file); Store.setActiveFiles(file);
}, },
closeTab(file) { closeTab(file) {
if (file.changed) return; if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
......
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Flash from '../../flash'; import Flash from '../../flash';
...@@ -8,6 +7,7 @@ const RepoHelper = { ...@@ -8,6 +7,7 @@ const RepoHelper = {
getDefaultActiveFile() { getDefaultActiveFile() {
return { return {
id: '',
active: true, active: true,
binary: false, binary: false,
extension: '', extension: '',
...@@ -62,6 +62,7 @@ const RepoHelper = { ...@@ -62,6 +62,7 @@ const RepoHelper = {
}); });
RepoHelper.updateHistoryEntry(tree.url, title); RepoHelper.updateHistoryEntry(tree.url, title);
Store.path = tree.path;
}, },
setDirectoryToClosed(entry) { setDirectoryToClosed(entry) {
...@@ -96,8 +97,8 @@ const RepoHelper = { ...@@ -96,8 +97,8 @@ const RepoHelper = {
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']); if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) { if (data.path && !Store.isInitialRoot) {
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']); Store.isRoot = data.path === '/';
Store.isInitialRoot = Store.isRoot; Store.isInitialRoot = Store.isRoot;
} }
...@@ -110,7 +111,7 @@ const RepoHelper = { ...@@ -110,7 +111,7 @@ const RepoHelper = {
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
} else if (!Store.isPreviewView() && !data.render_error) { } else if (!Store.isPreviewView() && !data.render_error) {
Service.getRaw(data.raw_path) Service.getRaw(data)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data; data.plain = rawResponse.data;
...@@ -138,6 +139,10 @@ const RepoHelper = { ...@@ -138,6 +139,10 @@ const RepoHelper = {
addToDirectory(file, data) { addToDirectory(file, data) {
const tree = file || Store; const tree = file || Store;
// TODO: Figure out why `popstate` is being trigger in the specs
if (!tree.files) return;
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0)); const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
tree.files = files; tree.files = files;
...@@ -157,7 +162,18 @@ const RepoHelper = { ...@@ -157,7 +162,18 @@ const RepoHelper = {
}, },
serializeRepoEntity(type, entity, level = 0) { serializeRepoEntity(type, entity, level = 0) {
const { id, url, name, icon, last_commit, tree_url } = entity; const {
id,
url,
name,
icon,
last_commit,
tree_url,
path,
tempFile,
active,
opened,
} = entity;
return { return {
id, id,
...@@ -165,11 +181,14 @@ const RepoHelper = { ...@@ -165,11 +181,14 @@ const RepoHelper = {
name, name,
url, url,
tree_url, tree_url,
path,
level, level,
tempFile,
icon: `fa-${icon}`, icon: `fa-${icon}`,
files: [], files: [],
loading: false, loading: false,
opened: false, opened,
active,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
lastCommit: last_commit ? { lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`, url: `${Store.projectUrl}/commit/${last_commit.id}`,
...@@ -213,7 +232,7 @@ const RepoHelper = { ...@@ -213,7 +232,7 @@ const RepoHelper = {
}, },
findOpenedFileFromActive() { findOpenedFileFromActive() {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
}, },
getFileFromPath(path) { getFileFromPath(path) {
...@@ -223,6 +242,76 @@ const RepoHelper = { ...@@ -223,6 +242,76 @@ const RepoHelper = {
loadingError() { loadingError() {
Flash('Unable to load this content at this time.'); Flash('Unable to load this content at this time.');
}, },
openEditMode() {
Store.editMode = true;
Store.currentBlobView = 'repo-editor';
},
updateStorePath(path) {
Store.path = path;
},
findOrCreateEntry(type, tree, name) {
let exists = true;
let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name);
if (!foundEntry) {
foundEntry = RepoHelper.serializeRepoEntity(type, {
id: name,
name,
path: tree.path ? `${tree.path}/${name}` : name,
icon: type === 'tree' ? 'folder' : 'file-text-o',
tempFile: true,
opened: true,
active: true,
}, tree.level !== undefined ? tree.level + 1 : 0);
exists = false;
tree.files.push(foundEntry);
}
return {
entry: foundEntry,
exists,
};
},
removeAllTmpFiles(storeFilesKey) {
Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
},
createNewEntry(name, type) {
const originalPath = Store.path;
let entryName = name;
if (entryName.indexOf(`${originalPath}/`) !== 0) {
this.updateStorePath('');
} else {
entryName = entryName.replace(`${originalPath}/`, '');
}
if (entryName === '') return;
const fileName = type === 'tree' ? '.gitkeep' : entryName;
let tree = Store;
if (type === 'tree') {
const dirNames = entryName.split('/');
dirNames.forEach((dirName) => {
if (dirName === '') return;
tree = this.findOrCreateEntry('tree', tree, dirName).entry;
});
}
if ((type === 'tree' && tree.tempFile) || type === 'blob') {
const file = this.findOrCreateEntry('blob', tree, fileName);
if (!file.exists) {
this.setFile(file.entry, file.entry);
this.openEditMode();
}
}
this.updateStorePath(originalPath);
},
}; };
export default RepoHelper; export default RepoHelper;
...@@ -5,6 +5,8 @@ import Service from './services/repo_service'; ...@@ -5,6 +5,8 @@ import Service from './services/repo_service';
import Store from './stores/repo_store'; import Store from './stores/repo_store';
import Repo from './components/repo.vue'; import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue'; import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
...@@ -27,6 +29,7 @@ function setInitialStore(data) { ...@@ -27,6 +29,7 @@ function setInitialStore(data) {
Store.service = Service; Store.service = Service;
Store.service.url = data.url; Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl; Store.service.refsUrl = data.refsUrl;
Store.path = data.currentPath;
Store.projectId = data.projectId; Store.projectId = data.projectId;
Store.projectName = data.projectName; Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
...@@ -62,9 +65,42 @@ function initRepoEditButton(el) { ...@@ -62,9 +65,42 @@ function initRepoEditButton(el) {
}); });
} }
function initNewDropdown(el) {
return new Vue({
el,
components: {
newDropdown,
},
render(createElement) {
return createElement('new-dropdown');
},
});
}
function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown');
if (!el) return null;
return new Vue({
el,
components: {
newBranchForm,
},
render(createElement) {
return createElement('new-branch-form', {
props: {
currentBranch: Store.currentBranch,
},
});
},
});
}
function initRepoBundle() { function initRepoBundle() {
const repo = document.getElementById('repo'); const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode'); const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
setInitialStore(repo.dataset); setInitialStore(repo.dataset);
addEventsForNonVueEls(); addEventsForNonVueEls();
initDropdowns(); initDropdowns();
...@@ -73,6 +109,8 @@ function initRepoBundle() { ...@@ -73,6 +109,8 @@ function initRepoBundle() {
initRepo(repo); initRepo(repo);
initRepoEditButton(editButton); initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);
} }
$(initRepoBundle); $(initRepoBundle);
......
...@@ -8,7 +8,7 @@ const RepoMixin = { ...@@ -8,7 +8,7 @@ const RepoMixin = {
changedFiles() { changedFiles() {
const changedFileList = this.openedFiles const changedFileList = this.openedFiles
.filter(file => file.changed); .filter(file => file.changed || file.tempFile);
return changedFileList; return changedFileList;
}, },
}, },
......
import axios from 'axios'; import axios from 'axios';
import csrf from '../../lib/utils/csrf';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper'; import Helper from '../helpers/repo_helper';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
const RepoService = { const RepoService = {
url: '', url: '',
options: { options: {
...@@ -10,10 +13,17 @@ const RepoService = { ...@@ -10,10 +13,17 @@ const RepoService = {
format: 'json', format: 'json',
}, },
}, },
createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/, richExtensionRegExp: /md/,
getRaw(url) { getRaw(file) {
return axios.get(url, { if (file.tempFile) {
return Promise.resolve({
data: '',
});
}
return axios.get(file.raw_path, {
// Stop Axios from parsing a JSON file into a JS object // Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
...@@ -73,6 +83,12 @@ const RepoService = { ...@@ -73,6 +83,12 @@ const RepoService = {
.then(this.commitFlash); .then(this.commitFlash);
}, },
createBranch(payload) {
const url = Api.buildUrl(this.createBranchPath)
.replace(':id', Store.projectId);
return axios.post(url, payload);
},
commitFlash(data) { commitFlash(data) {
if (data.short_id && data.stats) { if (data.short_id && data.stats) {
window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
......
...@@ -13,6 +13,7 @@ const RepoStore = { ...@@ -13,6 +13,7 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
branchUrl: '',
blobRaw: '', blobRaw: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
...@@ -38,6 +39,7 @@ const RepoStore = { ...@@ -38,6 +39,7 @@ const RepoStore = {
newMrTemplateUrl: '', newMrTemplateUrl: '',
branchChanged: false, branchChanged: false,
commitMessage: '', commitMessage: '',
path: '',
loading: { loading: {
tree: false, tree: false,
blob: false, blob: false,
...@@ -76,21 +78,23 @@ const RepoStore = { ...@@ -76,21 +78,23 @@ const RepoStore = {
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
Service.getRaw(file.raw_path) Service.getRaw(file)
.then((rawResponse) => { .then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data; RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data; Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError); }).catch(Helper.loadingError);
} }
if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); if (!file.loading && !file.tempFile) {
Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
}
RepoStore.binary = file.binary; RepoStore.binary = file.binary;
RepoStore.setActiveLine(-1); RepoStore.setActiveLine(-1);
}, },
setFileActivity(file, openedFile, i) { setFileActivity(file, openedFile, i) {
const activeFile = openedFile; const activeFile = openedFile;
activeFile.active = file.url === activeFile.url; activeFile.active = file.id === activeFile.id;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i); if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
...@@ -98,7 +102,7 @@ const RepoStore = { ...@@ -98,7 +102,7 @@ const RepoStore = {
}, },
setActiveFile(activeFile, i) { setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile); RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
RepoStore.activeFileIndex = i; RepoStore.activeFileIndex = i;
}, },
...@@ -120,6 +124,11 @@ const RepoStore = { ...@@ -120,6 +124,11 @@ const RepoStore = {
return openedFile.path !== file.path; return openedFile.path !== file.path;
}); });
// remove the file from the sidebar if it is a tempFile
if (file.tempFile) {
RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path));
}
// now activate the right tab based on what you closed. // now activate the right tab based on what you closed.
if (RepoStore.openedFiles.length === 0) { if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {}; RepoStore.activeFile = {};
...@@ -169,7 +178,7 @@ const RepoStore = { ...@@ -169,7 +178,7 @@ const RepoStore = {
// getters // getters
isActiveFile(file) { isActiveFile(file) {
return file && file.url === RepoStore.activeFile.url; return file && file.id === RepoStore.activeFile.id;
}, },
isPreviewView() { isPreviewView() {
......
...@@ -286,6 +286,7 @@ export default { ...@@ -286,6 +286,7 @@ export default {
<input <input
id="remove-source-branch-input" id="remove-source-branch-input"
v-model="removeSourceBranch" v-model="removeSourceBranch"
class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled" :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch type="checkbox"/> Remove source branch
</label> </label>
...@@ -311,8 +312,8 @@ export default { ...@@ -311,8 +312,8 @@ export default {
</button> </button>
</template> </template>
<template v-else> <template v-else>
<span class="bold"> <span class="bold js-resolve-mr-widget-items-message">
The pipeline for this merge request has not succeeded yet You can only merge once the items above are resolved
</span> </span>
</template> </template>
</div> </div>
......
<script> <script>
import ciIcon from './ci_icon.vue'; import ciIcon from './ci_icon.vue';
/** import tooltip from '../directives/tooltip';
* Renders CI Badge link with CI icon and status text based on /**
* API response shared between all places where it is used. * Renders CI Badge link with CI icon and status text based on
* * API response shared between all places where it is used.
* Receives status object containing: *
* status: { * Receives status object containing:
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url * status: {
* group:"running" // used for CSS class * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* icon: "icon_status_running" // used to render the icon * group:"running" // used for CSS class
* label:"running" // used for potential tooltip * icon: "icon_status_running" // used to render the icon
* text:"running" // text rendered * label:"running" // used for potential tooltip
* } * text:"running" // text rendered
* * }
* Used in: *
* - Pipelines table - first column * Used in:
* - Jobs table - first column * - Pipelines table - first column
* - Pipeline show view - header * - Jobs table - first column
* - Job show view - header * - Pipeline show view - header
* - MR widget * - Job show view - header
*/ * - MR widget
*/
export default { export default {
props: { props: {
status: { status: {
type: Object, type: Object,
required: true, required: true,
},
showText: {
type: Boolean,
required: false,
default: true,
},
}, },
}, components: {
ciIcon,
components: { },
ciIcon, directives: {
}, tooltip,
},
computed: { computed: {
cssClass() { cssClass() {
const className = this.status.group; const className = this.status.group;
return className ? `ci-status ci-${this.status.group}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
},
}, },
}, };
};
</script> </script>
<template> <template>
<a <a
:href="status.details_path" :href="status.details_path"
:class="cssClass"> :class="cssClass"
v-tooltip
:title="!showText ? status.text : ''">
<ci-icon :status="status" /> <ci-icon :status="status" />
{{status.text}}
<template v-if="showText">
{{status.text}}
</template>
</a> </a>
</template> </template>
...@@ -63,14 +63,17 @@ ...@@ -63,14 +63,17 @@
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
showBranch: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { computed: {
/** /**
* Used to verify if all the properties needed to render the commit * Used to verify if all the properties needed to render the commit
* ref section were provided. * ref section were provided.
* *
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean} * @returns {Boolean}
*/ */
hasCommitRef() { hasCommitRef() {
...@@ -80,8 +83,6 @@ ...@@ -80,8 +83,6 @@
* Used to verify if all the properties needed to render the commit * Used to verify if all the properties needed to render the commit
* author section were provided. * author section were provided.
* *
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean} * @returns {Boolean}
*/ */
hasAuthor() { hasAuthor() {
...@@ -114,31 +115,30 @@ ...@@ -114,31 +115,30 @@
</script> </script>
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
<div <template v-if="hasCommitRef && showBranch">
v-if="hasCommitRef" <div
class="icon-container hidden-xs"> class="icon-container hidden-xs">
<i <i
v-if="tag" v-if="tag"
class="fa fa-tag" class="fa fa-tag"
aria-hidden="true"> aria-hidden="true">
</i> </i>
<i <i
v-if="!tag" v-if="!tag"
class="fa fa-code-fork" class="fa fa-code-fork"
aria-hidden="true"> aria-hidden="true">
</i> </i>
</div> </div>
<a
v-if="hasCommitRef"
class="ref-name hidden-xs"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
:title="commitRef.name">
{{commitRef.name}}
</a>
<a
class="ref-name hidden-xs"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
:title="commitRef.name">
{{commitRef.name}}
</a>
</template>
<div <div
v-html="commitIconSvg" v-html="commitIconSvg"
class="commit-icon js-commit-icon"> class="commit-icon js-commit-icon">
......
<script>
/* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need
to show the loading status by setting the `loading` option.
This can also be used for initial page load when you don't
know the action of the button yet by setting
`loading: true, label: undefined`.
Sample configuration:
<loading-button
:loading="true"
:label="Hello"
@click="..."
/>
*/
import loadingIcon from './loading_icon.vue';
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
},
},
components: {
loadingIcon,
},
methods: {
onClick(e) {
this.$emit('click', e);
},
},
};
</script>
<template>
<button
class="btn btn-align-content"
@click="onClick"
type="button"
:disabled="loading"
>
<transition name="fade">
<loading-icon
v-if="loading"
:inline="true"
class="js-loading-button-icon"
:class="{
'append-right-5': label
}"
/>
</transition>
<transition name="fade">
<span
v-if="label"
class="js-loading-button-label"
>
{{ label }}
</span>
</transition>
</button>
</template>
<script> <script>
/**
* Common component to render a placeholder note and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `getUserData` getter that contains
* {
* path: String,
* avatar_url: String,
* name: String,
* username: String,
* }
*
* @example
* <placeholder-note
* :note="{body: 'This is a note'}"
* />
*/
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default { export default {
name: 'issuePlaceholderNote', name: 'placeholderNote',
props: { props: {
note: { note: {
type: Object, type: Object,
......
<script> <script>
/**
* Common component to render a placeholder system note.
*
* @example
* <placeholder-system-note
* :note="{ body: 'Commands are being applied'}"
* />
*/
export default { export default {
name: 'placeholderSystemNote', name: 'placeholderSystemNote',
props: { props: {
......
<script> <script>
/**
* Common component to render a system note, icon and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `targetNoteHash` getter
*
* @example
* <system-note
* :note="{
* id: String,
* author: Object,
* createdAt: String,
* note_html: String,
* system_note_icon_name: String
* }"
* />
*/
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import issueNoteHeader from './issue_note_header.vue'; import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
export default { export default {
name: 'systemNote', name: 'systemNote',
...@@ -24,7 +42,7 @@ ...@@ -24,7 +42,7 @@
return this.targetNoteHash === this.noteAnchorId; return this.targetNoteHash === this.noteAnchorId;
}, },
iconHtml() { iconHtml() {
return gl.utils.spriteIcon(this.note.system_note_icon_name); return spriteIcon(this.note.system_note_icon_name);
}, },
}, },
}; };
...@@ -46,7 +64,8 @@ ...@@ -46,7 +64,8 @@
:author="note.author" :author="note.author"
:created-at="note.created_at" :created-at="note.created_at"
:note-id="note.id" :note-id="note.id"
:action-text-html="note.note_html" /> :action-text-html="note.note_html"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
}, },
text: { text: {
type: String, type: String,
required: true, required: false,
}, },
kind: { kind: {
type: String, type: String,
...@@ -82,14 +82,15 @@ export default { ...@@ -82,14 +82,15 @@ export default {
type="button" type="button"
class="btn" class="btn"
:class="btnCancelKindClass" :class="btnCancelKindClass"
@click="emitSubmit(false)"> @click="close">
{{closeButtonLabel}} {{ closeButtonLabel }}
</button> </button>
<button type="button" <button
type="button"
class="btn" class="btn"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit(true)">
{{primaryButtonLabel}} {{ primaryButtonLabel }}
</button> </button>
</div> </div>
</div> </div>
......
...@@ -12,12 +12,14 @@ ...@@ -12,12 +12,14 @@
:img-alt="tooltipText" :img-alt="tooltipText"
:img-size="20" :img-size="20"
:tooltip-text="tooltipText" :tooltip-text="tooltipText"
tooltip-placement="top" :tooltip-placement="top"
:username="username"
/> />
*/ */
import userAvatarImage from './user_avatar_image.vue'; import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
export default { export default {
name: 'UserAvatarLink', name: 'UserAvatarLink',
...@@ -60,6 +62,22 @@ export default { ...@@ -60,6 +62,22 @@ export default {
required: false, required: false,
default: 'top', default: 'top',
}, },
username: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldShowUsername() {
return this.username.length > 0;
},
avatarTooltipText() {
return this.shouldShowUsername ? '' : this.tooltipText;
},
},
directives: {
tooltip,
}, },
}; };
</script> </script>
...@@ -73,8 +91,13 @@ export default { ...@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt" :img-alt="imgAlt"
:css-classes="imgCssClasses" :css-classes="imgCssClasses"
:size="imgSize" :size="imgSize"
:tooltip-text="tooltipText" :tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
v-if="shouldShowUsername"
v-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement" :tooltip-placement="tooltipPlacement"
/> >{{username}}</span>
</a> </a>
</template> </template>
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
@import "framework/layout"; @import "framework/layout";
@import "framework/animations"; @import "framework/animations";
@import "framework/vue_transitions";
@import "framework/avatar"; @import "framework/avatar";
@import "framework/asciidoctor"; @import "framework/asciidoctor";
@import "framework/banner"; @import "framework/banner";
...@@ -36,7 +37,7 @@ ...@@ -36,7 +37,7 @@
@import "framework/secondary-navigation-elements"; @import "framework/secondary-navigation-elements";
@import "framework/selects"; @import "framework/selects";
@import "framework/sidebar"; @import "framework/sidebar";
@import "framework/new-sidebar"; @import "framework/contextual-sidebar";
@import "framework/tables"; @import "framework/tables";
@import "framework/notes"; @import "framework/notes";
@import "framework/tabs"; @import "framework/tabs";
......
...@@ -292,6 +292,11 @@ ...@@ -292,6 +292,11 @@
} }
} }
.btn-align-content {
display: flex;
align-items: center;
}
.btn-group { .btn-group {
&.btn-grouped { &.btn-grouped {
@include btn-with-margin; @include btn-with-margin;
......
@import "framework/variables"; .page-with-contextual-sidebar {
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
$active-background: rgba(0, 0, 0, .04);
$active-hover-background: $active-background;
$active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08);
$hover-background: rgba(0, 0, 0, .06);
$hover-color: $gl-text-color;
$inactive-color: $gl-text-color-secondary;
$new-sidebar-width: 220px;
$new-sidebar-collapsed-width: 50px;
.page-with-new-sidebar {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-left: $new-sidebar-collapsed-width; padding-left: $contextual-sidebar-collapsed-width;
} }
@media (min-width: $screen-lg-min) { @media (min-width: $screen-lg-min) {
padding-left: $new-sidebar-width; padding-left: $contextual-sidebar-width;
} }
// Override position: absolute // Override position: absolute
...@@ -34,7 +20,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -34,7 +20,7 @@ $new-sidebar-collapsed-width: 50px;
.page-with-icon-sidebar { .page-with-icon-sidebar {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-left: $new-sidebar-collapsed-width; padding-left: $contextual-sidebar-collapsed-width;
} }
} }
...@@ -52,12 +38,12 @@ $new-sidebar-collapsed-width: 50px; ...@@ -52,12 +38,12 @@ $new-sidebar-collapsed-width: 50px;
&:hover, &:hover,
a:hover { a:hover {
background-color: $hover-background; background-color: $link-hover-background;
color: $hover-color; color: $gl-text-color;
.settings-avatar { .settings-avatar {
svg { svg {
fill: $hover-color; fill: $gl-text-color;
} }
} }
} }
...@@ -85,7 +71,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -85,7 +71,7 @@ $new-sidebar-collapsed-width: 50px;
.nav-sidebar { .nav-sidebar {
position: fixed; position: fixed;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $contextual-sidebar-width;
transition: left $sidebar-transition-duration; transition: left $sidebar-transition-duration;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
...@@ -103,7 +89,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -103,7 +89,7 @@ $new-sidebar-collapsed-width: 50px;
&.sidebar-icons-only { &.sidebar-icons-only {
width: auto; width: auto;
min-width: $new-sidebar-collapsed-width; min-width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
overflow-x: hidden; overflow-x: hidden;
...@@ -149,11 +135,11 @@ $new-sidebar-collapsed-width: 50px; ...@@ -149,11 +135,11 @@ $new-sidebar-collapsed-width: 50px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 16px; padding: 12px 16px;
color: $inactive-color; color: $gl-text-color-secondary;
} }
svg { svg {
fill: $inactive-color; fill: $gl-text-color-secondary;
} }
} }
...@@ -168,7 +154,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -168,7 +154,7 @@ $new-sidebar-collapsed-width: 50px;
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
left: (-$new-sidebar-width); left: (-$contextual-sidebar-width);
} }
.nav-icon-container { .nav-icon-container {
...@@ -210,8 +196,8 @@ $new-sidebar-collapsed-width: 50px; ...@@ -210,8 +196,8 @@ $new-sidebar-collapsed-width: 50px;
&:hover, &:hover,
&:focus { &:focus {
background: $active-hover-background; background: $link-active-background;
color: $active-hover-color; color: $gl-text-color;
} }
} }
...@@ -220,7 +206,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -220,7 +206,7 @@ $new-sidebar-collapsed-width: 50px;
&, &,
&:hover, &:hover,
&:focus { &:focus {
background: $active-background; background: $link-active-background;
} }
} }
} }
...@@ -308,11 +294,11 @@ $new-sidebar-collapsed-width: 50px; ...@@ -308,11 +294,11 @@ $new-sidebar-collapsed-width: 50px;
.badge { .badge {
background-color: $inactive-badge-background; background-color: $inactive-badge-background;
color: $inactive-color; color: $gl-text-color-secondary;
} }
&.active { &.active {
background: $active-background; background: $link-active-background;
> a { > a {
margin-left: 4px; margin-left: 4px;
...@@ -330,7 +316,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -330,7 +316,7 @@ $new-sidebar-collapsed-width: 50px;
&.active > a:hover, &.active > a:hover,
&.is-over > a { &.is-over > a {
background-color: $hover-background; background-color: $link-hover-background;
} }
} }
} }
...@@ -340,7 +326,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -340,7 +326,7 @@ $new-sidebar-collapsed-width: 50px;
.toggle-sidebar-button, .toggle-sidebar-button,
.close-nav-button { .close-nav-button {
width: $new-sidebar-width - 2px; width: $contextual-sidebar-width - 2px;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
padding: 16px; padding: 16px;
...@@ -407,7 +393,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -407,7 +393,7 @@ $new-sidebar-collapsed-width: 50px;
} }
.toggle-sidebar-button { .toggle-sidebar-button {
width: $new-sidebar-collapsed-width - 2px; width: $contextual-sidebar-collapsed-width - 2px;
padding: 16px; padding: 16px;
.collapse-text, .collapse-text,
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
display: flex; display: flex;
flex: 1; flex: 1;
-webkit-flex: 1; -webkit-flex: 1;
padding-left: 30px; padding-left: 12px;
position: relative; position: relative;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -221,10 +221,6 @@ ...@@ -221,10 +221,6 @@
box-shadow: 0 0 4px $search-input-focus-shadow-color; box-shadow: 0 0 4px $search-input-focus-shadow-color;
} }
&.focus .fa-filter {
color: $common-gray-dark;
}
gl-emoji { gl-emoji {
display: inline-block; display: inline-block;
font-family: inherit; font-family: inherit;
...@@ -251,13 +247,6 @@ ...@@ -251,13 +247,6 @@
} }
} }
.fa-filter {
position: absolute;
top: 10px;
left: 10px;
color: $gray-darkest;
}
.fa-times { .fa-times {
right: 10px; right: 10px;
color: $gray-darkest; color: $gray-darkest;
......
...@@ -9,6 +9,8 @@ $sidebar-transition-duration: .15s; ...@@ -9,6 +9,8 @@ $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px; $sidebar-breakpoint: 1024px;
$default-transition-duration: .15s; $default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s; $right-sidebar-transition-duration: .3s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
/* /*
* Color schema * Color schema
...@@ -358,6 +360,13 @@ $dropdown-item-hover-bg: $gray-darker; ...@@ -358,6 +360,13 @@ $dropdown-item-hover-bg: $gray-darker;
$filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09); $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09);
$dropdown-hover-color: $blue-400; $dropdown-hover-color: $blue-400;
/*
* Contextual Sidebar
*/
$link-active-background: rgba(0, 0, 0, .04);
$link-hover-background: rgba(0, 0, 0, .06);
$inactive-badge-background: rgba(0, 0, 0, .08);
/* /*
* Buttons * Buttons
*/ */
...@@ -404,7 +413,6 @@ $note-targe3-inside: #ffffd3; ...@@ -404,7 +413,6 @@ $note-targe3-inside: #ffffd3;
$note-line2-border: #ddd; $note-line2-border: #ddd;
$note-icon-gutter-width: 55px; $note-icon-gutter-width: 55px;
/* /*
* Zen * Zen
*/ */
......
.fade-enter-active,
.fade-leave-active {
transition: opacity $sidebar-transition-duration $general-hover-transition-curve;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
...@@ -414,7 +414,7 @@ ...@@ -414,7 +414,7 @@
margin: 5px; margin: 5px;
} }
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { .page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar {
.issuable-sidebar-header { .issuable-sidebar-header {
position: relative; position: relative;
} }
......
...@@ -249,13 +249,12 @@ ...@@ -249,13 +249,12 @@
width: 100%; width: 100%;
padding-right: 5px; padding-right: 5px;
} }
} }
.discussion-actions { .discussion-actions {
display: table; display: table;
.new-issue-for-discussion path { .btn-default path {
fill: $gray-darkest; fill: $gray-darkest;
} }
......
...@@ -466,6 +466,10 @@ ul.notes { ...@@ -466,6 +466,10 @@ ul.notes {
float: right; float: right;
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
} }
.note-actions { .note-actions {
......
.fade-enter-active,
.fade-leave-active {
transition: opacity $sidebar-transition-duration;
}
.monaco-loader { .monaco-loader {
position: absolute; position: absolute;
top: 0; top: 0;
...@@ -206,7 +201,7 @@ ...@@ -206,7 +201,7 @@
} }
} }
#repo-file-buttons { .repo-file-buttons {
background-color: $white-light; background-color: $white-light;
padding: 5px 10px; padding: 5px 10px;
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
......
...@@ -57,6 +57,10 @@ class HelpController < ApplicationController ...@@ -57,6 +57,10 @@ class HelpController < ApplicationController
def shortcuts def shortcuts
end end
def instance_configuration
@instance_configuration = InstanceConfiguration.new
end
def ui def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe') @user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end end
......
...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
tree_path = path_segments.join('/') tree_path = path_segments.join('/')
render json: json.merge( render json: json.merge(
id: @blob.id,
path: blob.path, path: blob.path,
name: blob.name, name: blob.name,
extension: blob.extension, extension: blob.extension,
......
...@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names =
repository.merged_branch_names(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch| @max_commits = @branches.reduce(0) do |memo, branch|
......
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:update, :move] before_action :authorize_update_issue!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end end
def edit
respond_with(@issue)
end
def show def show
@noteable = @issue @noteable = @issue
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
...@@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format| respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do format.json do
render_issue_json render_issue_json
end end
......
...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
format.json do format.json do
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
response.header['is-root'] = @path.empty?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
......
...@@ -23,7 +23,7 @@ class BranchesFinder ...@@ -23,7 +23,7 @@ class BranchesFinder
def filter_by_name(branches) def filter_by_name(branches)
if search if search
branches.select { |branch| branch.name.include?(search) } branches.select { |branch| branch.name.upcase.include?(search.upcase) }
else else
branches branches
end end
......
...@@ -120,6 +120,15 @@ module ApplicationSettingsHelper ...@@ -120,6 +120,15 @@ module ApplicationSettingsHelper
message.html_safe message.html_safe
end end
def circuitbreaker_access_retries_help_text
_('The number of attempts GitLab will make to access a storage.')
end
def circuitbreaker_backoff_threshold_help_text
_("The number of failures after which GitLab will start temporarily "\
"disabling access to a storage shard on a host")
end
def circuitbreaker_failure_wait_time_help_text def circuitbreaker_failure_wait_time_help_text
_("When access to a storage fails. GitLab will prevent access to the "\ _("When access to a storage fails. GitLab will prevent access to the "\
"storage for the time specified here. This allows the filesystem to "\ "storage for the time specified here. This allows the filesystem to "\
...@@ -144,6 +153,8 @@ module ApplicationSettingsHelper ...@@ -144,6 +153,8 @@ module ApplicationSettingsHelper
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:circuitbreaker_access_retries,
:circuitbreaker_backoff_threshold,
:circuitbreaker_failure_count_threshold, :circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_reset_time, :circuitbreaker_failure_reset_time,
:circuitbreaker_failure_wait_time, :circuitbreaker_failure_wait_time,
......
module InstanceConfigurationHelper
def instance_configuration_cell_html(value, &block)
return '-' unless value.to_s.presence
block_given? ? yield(value) : value
end
def instance_configuration_host(host)
@instance_configuration_host ||= instance_configuration_cell_html(host).capitalize
end
# Value must be in bytes
def instance_configuration_human_size_cell(value)
instance_configuration_cell_html(value) do |v|
number_to_human_size(v, strip_insignificant_zeros: true, significant: false)
end
end
end
module NavHelper module NavHelper
def page_with_sidebar_class def page_with_sidebar_class
class_name = page_gutter_class class_name = page_gutter_class
class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name class_name
......
...@@ -16,17 +16,16 @@ module StorageHealthHelper ...@@ -16,17 +16,16 @@ module StorageHealthHelper
def message_for_circuit_breaker(circuit_breaker) def message_for_circuit_breaker(circuit_breaker)
maximum_failures = circuit_breaker.failure_count_threshold maximum_failures = circuit_breaker.failure_count_threshold
current_failures = circuit_breaker.failure_count current_failures = circuit_breaker.failure_count
permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures
translation_params = { number_of_failures: current_failures, translation_params = { number_of_failures: current_failures,
maximum_failures: maximum_failures, maximum_failures: maximum_failures,
number_of_seconds: circuit_breaker.failure_wait_time } number_of_seconds: circuit_breaker.failure_wait_time }
if permanently_broken if circuit_breaker.circuit_broken?
s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\
"retry automatically. Reset storage information when the problem is "\ "retry automatically. Reset storage information when the problem is "\
"resolved.") % translation_params "resolved.") % translation_params
elsif circuit_breaker.circuit_broken? elsif circuit_breaker.backing_off?
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"block access for %{number_of_seconds} seconds.") % translation_params "block access for %{number_of_seconds} seconds.") % translation_params
else else
......
...@@ -153,13 +153,25 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -153,13 +153,25 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :circuitbreaker_failure_count_threshold, validates :circuitbreaker_backoff_threshold,
:circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_wait_time, :circuitbreaker_failure_wait_time,
:circuitbreaker_failure_reset_time, :circuitbreaker_failure_reset_time,
:circuitbreaker_storage_timeout, :circuitbreaker_storage_timeout,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :circuitbreaker_access_retries,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1 }
validates_each :circuitbreaker_backoff_threshold do |record, attr, value|
if value.to_i >= record.circuitbreaker_failure_count_threshold
record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\
"lower than the failure count threshold"))
end
end
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
......
...@@ -249,9 +249,7 @@ module Ci ...@@ -249,9 +249,7 @@ module Ci
end end
def commit def commit
@commit ||= project.commit(sha) @commit ||= project.commit_by(oid: sha)
rescue
nil
end end
def branch? def branch?
......
...@@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base ...@@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base
end end
def ref_path def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}"
end end
def formatted_external_url def formatted_external_url
......
require 'resolv'
class InstanceConfiguration
SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze
SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze
CACHE_KEY = 'instance_configuration'.freeze
EXPIRATION_TIME = 24.hours
def settings
@configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
gitlab_ci: gitlab_ci }.deep_symbolize_keys
end
end
private
def ssh_algorithms_hashes
SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact
end
def host
Settings.gitlab.host
end
def gitlab_pages
Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host))
end
def resolv_dns(dns)
Resolv.getaddress(dns)
rescue Resolv::ResolvError
end
def gitlab_ci
Settings.gitlab_ci
.to_h
.merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes,
default: 100.megabytes })
end
def ssh_algorithm_file(algorithm)
File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
end
def ssh_algorithm_hashes(algorithm)
content = ssh_algorithm_file_content(algorithm)
return unless content.present?
{ name: algorithm,
md5: ssh_algorithm_md5(content),
sha256: ssh_algorithm_sha256(content) }
end
def ssh_algorithm_file_content(algorithm)
file = ssh_algorithm_file(algorithm)
return unless File.exist?(file)
File.read(file)
end
def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
end
def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
end
end
...@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base ...@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base, key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc' algorithm: 'aes-256-cbc'
after_create :update after_create :update_daemon
after_save :update after_save :update_daemon
after_destroy :update after_destroy :update_daemon
def to_param def to_param
domain domain
...@@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base ...@@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base
private private
def update def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute ::Projects::UpdatePagesConfigurationService.new(project).execute
end end
......
...@@ -540,6 +540,10 @@ class Project < ActiveRecord::Base ...@@ -540,6 +540,10 @@ class Project < ActiveRecord::Base
repository.commit(ref) repository.commit(ref)
end end
def commit_by(oid:)
repository.commit_by(oid: oid)
end
# ref can't be HEAD, can only be branch/tag name or SHA # ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch) def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref) latest_pipeline = pipelines.latest_successful_for(ref)
...@@ -553,7 +557,7 @@ class Project < ActiveRecord::Base ...@@ -553,7 +557,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(first_commit_id, second_commit_id) def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id)
repository.commit(sha) if sha commit_by(oid: sha) if sha
end end
def saved? def saved?
......
...@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService ...@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated? validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true validates :api_url, url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
......
...@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService ...@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService
end end
def default_namespace def default_namespace
"#{project.path}-#{project.id}" if project.present? return unless project
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kubeclient!(api_path: 'api', api_version: 'v1')
......
...@@ -76,6 +76,7 @@ class Repository ...@@ -76,6 +76,7 @@ class Repository
@full_path = full_path @full_path = full_path
@disk_path = disk_path || full_path @disk_path = disk_path || full_path
@project = project @project = project
@commit_cache = {}
end end
def ==(other) def ==(other)
...@@ -103,18 +104,17 @@ class Repository ...@@ -103,18 +104,17 @@ class Repository
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
return ref if ref.is_a?(::Commit)
commit = find_commit(ref)
if ref.is_a?(Gitlab::Git::Commit) end
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
commit = ::Commit.new(commit, @project) if commit # Finding a commit by the passed SHA
commit # Also takes care of caching, based on the SHA
rescue Rugged::OdbError, Rugged::TreeError def commit_by(oid:)
nil return @commit_cache[oid] if @commit_cache.key?(oid)
@commit_cache[oid] = find_commit(oid)
end end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
...@@ -231,7 +231,7 @@ class Repository ...@@ -231,7 +231,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for # branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds. # example if they have comments or CI builds.
def keep_around(sha) def keep_around(sha)
return unless sha && commit(sha) return unless sha && commit_by(oid: sha)
return if kept_around?(sha) return if kept_around?(sha)
...@@ -862,22 +862,12 @@ class Repository ...@@ -862,22 +862,12 @@ class Repository
end end
def ff_merge(user, source, target_branch, merge_request: nil) def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target their_commit_id = commit(source)&.id
their_commit = raise 'Invalid merge source' if their_commit_id.nil?
if source.is_a?(Gitlab::Git::Commit)
source.raw_commit
else
rugged.lookup(source)
end
raise 'Invalid merge target' if our_commit.nil?
raise 'Invalid merge source' if their_commit.nil?
with_branch(user, target_branch) do |start_commit| merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
their_commit.oid with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
end end
def revert( def revert(
...@@ -912,18 +902,27 @@ class Repository ...@@ -912,18 +902,27 @@ class Repository
end end
end end
def merged_to_root_ref?(branch_name) def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
branch_commit = commit(branch_name) branch = Gitlab::Git::Branch.find(self, branch_or_name)
root_ref_commit = commit(root_ref)
if branch_commit if branch
same_head = branch_commit.id == root_ref_commit.id root_ref_sha = commit(root_ref).sha
!same_head && ancestor?(branch_commit.id, root_ref_commit.id) same_head = branch.target == root_ref_sha
merged =
if pre_loaded_merged_branches
pre_loaded_merged_branches.include?(branch.name)
else
ancestor?(branch.target, root_ref_sha)
end
!same_head && merged
else else
nil nil
end end
end end
delegate :merged_branch_names, to: :raw_repository
def merge_base(first_commit_id, second_commit_id) def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
...@@ -1031,6 +1030,10 @@ class Repository ...@@ -1031,6 +1030,10 @@ class Repository
if instance_variable_defined?(ivar) if instance_variable_defined?(ivar)
instance_variable_get(ivar) instance_variable_get(ivar)
else else
# If the repository doesn't exist and a fallback was specified we return
# that value inmediately. This saves us Rugged/gRPC invocations.
return fallback unless fallback.nil? || exists?
begin begin
value = value =
if memoize_only if memoize_only
...@@ -1040,8 +1043,9 @@ class Repository ...@@ -1040,8 +1043,9 @@ class Repository
end end
instance_variable_set(ivar, value) instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to # Even if the above `#exists?` check passes these errors might still
# gracefully handle this and not cache anything. # occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything
fallback fallback
end end
end end
...@@ -1069,6 +1073,18 @@ class Repository ...@@ -1069,6 +1073,18 @@ class Repository
private private
# TODO Generice finder, later split this on finders by Ref or Oid
# gitlab-org/gitlab-ce#39239
def find_commit(oid_or_ref)
commit = if oid_or_ref.is_a?(Gitlab::Git::Commit)
oid_or_ref
else
Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
end
::Commit.new(commit, @project) if commit
end
def blob_data_at(sha, path) def blob_data_at(sha, path)
blob = blob_at(sha, path) blob = blob_at(sha, path)
return unless blob return unless blob
...@@ -1107,12 +1123,12 @@ class Repository ...@@ -1107,12 +1123,12 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path) def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c) commit_by(oid: c)
end end
def last_commit_for_path_by_rugged(sha, path) def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path)
commit(sha) commit_by(oid: sha)
end end
def last_commit_id_for_path_by_shelling_out(sha, path) def last_commit_id_for_path_by_shelling_out(sha, path)
......
...@@ -16,8 +16,8 @@ module Users ...@@ -16,8 +16,8 @@ module Users
user_cache_key user_cache_key
] ]
if event.project.forked? if forked_from = event.project.forked_from_project
keys << project_cache_key(event.project.forked_from_project) keys << project_cache_key(forked_from)
end end
keys.each { |key| set_key(key, event.id) } keys.each { |key| set_key(key, event.id) }
......
...@@ -533,11 +533,23 @@ ...@@ -533,11 +533,23 @@
%fieldset %fieldset
%legend Git Storage Circuitbreaker settings %legend Git Storage Circuitbreaker settings
.form-group .form-group
= f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' = f.number_field :circuitbreaker_access_retries, class: 'form-control'
.help-block .help-block
= circuitbreaker_failure_count_help_text = circuitbreaker_access_retries_help_text
.form-group
= f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
.help-block
= circuitbreaker_storage_timeout_help_text
.form-group
= f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_backoff_threshold, class: 'form-control'
.help-block
= circuitbreaker_backoff_threshold_help_text
.form-group .form-group
= f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2' = f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
...@@ -545,17 +557,17 @@ ...@@ -545,17 +557,17 @@
.help-block .help-block
= circuitbreaker_failure_wait_time_help_text = circuitbreaker_failure_wait_time_help_text
.form-group .form-group
= f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
.help-block .help-block
= circuitbreaker_failure_reset_time_help_text = circuitbreaker_failure_count_help_text
.form-group .form-group
= f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= f.number_field :circuitbreaker_storage_timeout, class: 'form-control' = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
.help-block .help-block
= circuitbreaker_storage_timeout_help_text = circuitbreaker_failure_reset_time_help_text
%fieldset %fieldset
%legend Repository Checks %legend Repository Checks
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects Your projects
= nav_link(page: starred_dashboard_projects_path) do = nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
Starred projects Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do = link_to explore_root_path, data: {placement: 'right'} do
Explore projects Explore projects
.nav-controls .nav-controls
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
%span= Gitlab::VERSION %span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
= version_status_badge = version_status_badge
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
%br %br
...@@ -23,6 +24,7 @@ ...@@ -23,6 +24,7 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br %br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%hr %hr
.row.prepend-top-default .row.prepend-top-default
......
- page_title 'Instance Configuration'
.wiki.documentation
%h1 Instance Configuration
%p
In this page you will find information about the settings that are used in your current instance.
= render 'help/instance_configuration/ssh_info'
= render 'help/instance_configuration/gitlab_pages'
= render 'help/instance_configuration/gitlab_ci'
%p
%strong Table of contents
%ul
= content_for :table_content
= content_for :settings_content
- content_for :table_content do
%li= link_to 'GitLab CI', '#gitlab-ci'
- content_for :settings_content do
%h2#gitlab-ci
GitLab CI
%p
Below are the current settings regarding
= succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
.table-responsive
%table
%thead
%tr
%th Setting
%th= instance_configuration_host(@instance_configuration.settings[:host])
%th Default
%tbody
%tr
- artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
%td Artifacts maximum size
%td= instance_configuration_human_size_cell(artifacts_size[:value])
%td= instance_configuration_human_size_cell(artifacts_size[:default])
- gitlab_pages = @instance_configuration.settings[:gitlab_pages]
- content_for :table_content do
%li= link_to 'GitLab Pages', '#gitlab-pages'
- content_for :settings_content do
%h2#gitlab-pages
GitLab Pages
%p
Below are the settings for
= succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') }
.table-responsive
%table
%thead
%tr
%th Setting
%th= instance_configuration_host(@instance_configuration.settings[:host])
%tbody
%tr
%td Domain Name
%td
%code= instance_configuration_cell_html(gitlab_pages[:host])
%tr
%td IP Address
%td
%code= instance_configuration_cell_html(gitlab_pages[:ip_address])
%tr
%td Port
%td
%code= instance_configuration_cell_html(gitlab_pages[:port])
%br
%p
The maximum size of your Pages site is regulated by the artifacts maximum
size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }}
- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes]
- if ssh_info.any?
- content_for :table_content do
%li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints'
- content_for :settings_content do
%h2#ssh-host-keys-fingerprints
SSH host keys fingerprints
%p
Below are the fingerprints for the current instance SSH host keys.
.table-responsive
%table
%thead
%tr
%th Algorithm
%th MD5
%th SHA256
%tbody
- ssh_info.each do |algorithm|
%tr
%td= algorithm[:name]
%td
%code= instance_configuration_cell_html(algorithm[:md5])
%td
%code= instance_configuration_cell_html(algorithm[:sha256])
- local_assigns.fetch(:view)
%strong
%span{ data: { defer_to: "#{view.defer_key}-duration" } } ...
\/
%span{ data: { defer_to: "#{view.defer_key}-calls" } } ...
Gitaly
- merged = local_assigns.fetch(:merged, false)
- commit = @repository.commit(branch.dereferenced_target) - commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch) - diverging_commit_counts = @repository.diverging_commit_counts(branch)
...@@ -12,7 +13,7 @@ ...@@ -12,7 +13,7 @@
&nbsp; &nbsp;
- if branch.name == @repository.root_ref - if branch.name == @repository.root_ref
%span.label.label-primary default %span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name - elsif merged
%span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged') = s_('Branches|merged')
...@@ -47,7 +48,7 @@ ...@@ -47,7 +48,7 @@
target: "#modal-delete-branch", target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name), delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name, branch_name: branch.name,
is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } is_merged: ("true" if merged) } }
= icon("trash-o") = icon("trash-o")
- else - else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
- if @branches.any? - if @branches.any?
%ul.content-list.all-branches %ul.content-list.all-branches
- @branches.each do |branch| - @branches.each do |branch|
= render "projects/branches/branch", branch: branch = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names)
= paginate @branches, theme: 'gitlab' = paginate @branches, theme: 'gitlab'
- else - else
.nothing-here-block .nothing-here-block
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
- if can?(current_user, :update_cluster, @cluster) - if can?(current_user, :update_cluster, @cluster)
.form-group .form-group
= field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success' = field.submit _('Save'), class: 'btn btn-success'
%section.settings#js-cluster-details %section.settings#js-cluster-details
.settings-header .settings-header
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
%section.settings#js-cluster-advanced-settings %section.settings#js-cluster-advanced-settings
.settings-header .settings-header
%h4= s_('ClusterIntegration|Advanced settings') %h4= _('Advanced settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
......
...@@ -77,5 +77,6 @@ ...@@ -77,5 +77,6 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
.mr-widget-pipeline-graph .mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
in - if last_pipeline.duration
= time_interval_in_words last_pipeline.duration in
= time_interval_in_words last_pipeline.duration
...@@ -72,6 +72,7 @@ ...@@ -72,6 +72,7 @@
%pre.light-well %pre.light-well
:preserve :preserve
cd existing_repo cd existing_repo
git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git push -u origin --all git push -u origin --all
git push -u origin --tags git push -u origin --tags
......
- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
%hr
= render "form"
.tree-ref-container .tree-ref-container
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- unless show_new_repo? - if show_new_repo?
.js-new-dropdown
- else
= render 'projects/tree/old_tree_header' = render 'projects/tree/old_tree_header'
.tree-controls .tree-controls
......
- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch - dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
...@@ -7,8 +8,20 @@ ...@@ -7,8 +8,20 @@
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Switch branch/tag") .dropdown-page-one
= dropdown_filter _("Search branches and tags") = dropdown_title _("Switch branch/tag")
= dropdown_content = dropdown_filter _("Search branches and tags")
= dropdown_loading = dropdown_content
= dropdown_loading
- if show_new_branch_form
= dropdown_footer do
%ul.dropdown-footer-list
%li
%a.dropdown-toggle-page{ href: "#" }
Create new branch
- if show_new_branch_form
.dropdown-page-two
= dropdown_title("Create new branch", options: { back: true })
= dropdown_content do
.js-new-branch-dropdown
...@@ -14,5 +14,5 @@ ...@@ -14,5 +14,5 @@
= link_to_member(@project, participant, name: false, size: 24, lazy_load: true) = link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- if participants_extra > 0 - if participants_extra > 0
.hide-collapsed.participants-more .hide-collapsed.participants-more
%a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } } %button.btn-transparent.btn-blank.js-participants-more{ type: 'button', data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+ #{participants_extra} more + #{participants_extra} more
...@@ -25,7 +25,6 @@ ...@@ -25,7 +25,6 @@
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) } %input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } } %li.filter-dropdown-item{ data: { action: 'submit' } }
......
...@@ -7,4 +7,5 @@ ...@@ -7,4 +7,5 @@
blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'), blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }), new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s, can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
current_path: @path } }
---
title: Suggest to rename the remote for existing repository instructions
merge_request: 14970
author: helmo42
type: added
---
title: Add API endpoints for Pages Domains
merge_request: 13917
author: Travis Miller
type: added
---
title: Remove filter icon from search bar
merge_request:
author:
type: other
---
title: Case insensitive search for branches
merge_request: 14995
author: George Andrinopoulos
type: fixed
---
title: Refactor have_http_status into have_gitlab_http_status
merge_request: 14958
author: Jacopo Beschi @jacopo-beschi
type: added
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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