summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMike Greiling <mike@pixelcog.com>2017-10-29 20:51:34 -0500
committerMike Greiling <mike@pixelcog.com>2017-10-29 20:51:34 -0500
commit6a92dff2bc368aa362a0e239f9fcf953ad4e9e52 (patch)
tree458a7621335bf04895223cd9789729d0e3e68a13 /app
parent40ee89bc7da8b1c0500f2358d2548f2395bc2323 (diff)
parent1aae91cf24606275834d6e17279ce14c1074dade (diff)
downloadgitlab-ce-6a92dff2bc368aa362a0e239f9fcf953ad4e9e52.tar.gz
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" ...
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue6
-rw-r--r--app/assets/javascripts/contextual_sidebar.js (renamed from app/assets/javascripts/new_sidebar.js)4
-rw-r--r--app/assets/javascripts/diff.js15
-rw-r--r--app/assets/javascripts/dispatcher.js25
-rw-r--r--app/assets/javascripts/files_comment_button.js9
-rw-r--r--app/assets/javascripts/group_avatar.js31
-rw-r--r--app/assets/javascripts/group_label_subscription.js11
-rw-r--r--app/assets/javascripts/groups_select.js185
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js4
-rw-r--r--app/assets/javascripts/issuable_context.js19
-rw-r--r--app/assets/javascripts/layout_nav.js6
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js8
-rw-r--r--app/assets/javascripts/main.js5
-rw-r--r--app/assets/javascripts/merge_request_tabs.js15
-rw-r--r--app/assets/javascripts/notes.js8
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/issue_notes_app.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue23
-rw-r--r--app/assets/javascripts/repo/components/new_branch_form.vue115
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue86
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/modal.vue90
-rw-r--r--app/assets/javascripts/repo/components/repo.vue36
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue6
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue2
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue12
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue6
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js103
-rw-r--r--app/assets/javascripts/repo/index.js38
-rw-r--r--app/assets/javascripts/repo/mixins/repo_mixin.js2
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js20
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue92
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue71
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue (renamed from app/assets/javascripts/notes/components/issue_placeholder_note.vue)21
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue (renamed from app/assets/javascripts/notes/components/issue_placeholder_system_note.vue)8
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue (renamed from app/assets/javascripts/notes/components/issue_system_note.vue)25
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue29
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/buttons.scss5
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss (renamed from app/assets/stylesheets/framework/new-sidebar.scss)54
-rw-r--r--app/assets/stylesheets/framework/filters.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss10
-rw-r--r--app/assets/stylesheets/framework/vue_transitions.scss9
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss7
-rw-r--r--app/controllers/help_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb10
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/finders/branches_finder.rb2
-rw-r--r--app/helpers/application_settings_helper.rb11
-rw-r--r--app/helpers/instance_configuration_helper.rb18
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/storage_health_helper.rb5
-rw-r--r--app/models/application_setting.rb14
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/instance_configuration.rb71
-rw-r--r--app/models/pages_domain.rb8
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb5
-rw-r--r--app/models/repository.rb86
-rw-r--r--app/services/users/last_push_event_service.rb4
-rw-r--r--app/views/admin/application_settings/_form.html.haml30
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/help/instance_configuration.html.haml17
-rw-r--r--app/views/help/instance_configuration/_gitlab_ci.html.haml24
-rw-r--r--app/views/help/instance_configuration/_gitlab_pages.html.haml35
-rw-r--r--app/views/help/instance_configuration/_ssh_info.html.haml27
-rw-r--r--app/views/peek/views/_gitaly.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml5
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/clusters/show.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/empty.html.haml1
-rw-r--r--app/views/projects/issues/edit.html.haml7
-rw-r--r--app/views/projects/tree/_tree_header.html.haml6
-rw-r--r--app/views/shared/_ref_switcher.html.haml23
-rw-r--r--app/views/shared/issuable/_participants.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml1
-rw-r--r--app/views/shared/repo/_repo.html.haml3
91 files changed, 1373 insertions, 460 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 0661087a1ba..e9a0dbaa59d 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -25,6 +25,11 @@
type: String,
required: true,
},
+ viewType: {
+ type: String,
+ required: false,
+ default: 'child',
+ },
},
mixins: [
pipelinesMixin,
@@ -110,6 +115,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
+ :view-type="viewType"
/>
</div>
</div>
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 997550b37fb..46b68ebe158 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
import _ from 'underscore';
import bp from './breakpoints';
-export default class NewNavSidebar {
+export default class ContextualSidebar {
constructor() {
this.initDomElements();
this.render();
@@ -55,7 +55,7 @@ export default class NewNavSidebar {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
- NewNavSidebar.setCollapsedCookie(collapsed);
+ ContextualSidebar.setCollapsedCookie(collapsed);
this.toggleSidebarOverflow();
}
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 6c78662baa7..c8874e48c09 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,5 +1,3 @@
-/* eslint-disable class-methods-use-this */
-
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
@@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
-class Diff {
+export default class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
@@ -104,7 +102,7 @@ class Diff {
}
this.highlightSelectedLine();
}
-
+ // eslint-disable-next-line class-methods-use-this
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
@@ -116,11 +114,11 @@ class Diff {
table.addClass(`${lineClass}-selected`);
}
}
-
+ // eslint-disable-next-line class-methods-use-this
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
-
+ // eslint-disable-next-line class-methods-use-this
lineNumbers(line) {
const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) {
@@ -128,7 +126,7 @@ class Diff {
}
return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
-
+ // eslint-disable-next-line class-methods-use-this
highlightSelectedLine() {
const hash = gl.utils.getLocationHash();
const $diffFiles = $('.diff-file');
@@ -141,6 +139,3 @@ class Diff {
}
}
}
-
-window.gl = window.gl || {};
-window.gl.Diff = Diff;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 2885923aeda..970e83c0ecb 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -8,11 +8,12 @@
/* global NewBranchForm */
/* global NotificationsForm */
/* global NotificationsDropdown */
-/* global GroupAvatar */
+import groupAvatar from './group_avatar';
+import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
-/* global GroupsSelect */
+import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
/* global NamespaceSelects */
@@ -87,6 +88,7 @@ import U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
+import Diff from './diff';
(function() {
var Dispatcher;
@@ -237,8 +239,9 @@ import DueDateSelectors from './due_date_select';
new GLForm($('.milestone-form'), true);
break;
case 'projects:compare:show':
- new gl.Diff();
- initChangesDropdown();
+ new Diff();
+ const paddingTop = 16;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
break;
case 'projects:branches:new':
case 'projects:branches:create':
@@ -273,7 +276,7 @@ import DueDateSelectors from './due_date_select';
}
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
- new gl.Diff();
+ new Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
@@ -307,7 +310,7 @@ import DueDateSelectors from './due_date_select';
new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
- new gl.Diff();
+ new Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
@@ -323,7 +326,7 @@ import DueDateSelectors from './due_date_select';
new gl.Activities();
break;
case 'projects:commit:show':
- new gl.Diff();
+ new Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({
@@ -411,7 +414,7 @@ import DueDateSelectors from './due_date_select';
break;
case 'projects:project_members:index':
memberExpirationDate('.js-access-expiration-date-groups');
- new GroupsSelect();
+ groupsSelect();
memberExpirationDate();
new Members();
new UsersSelect();
@@ -422,11 +425,11 @@ import DueDateSelectors from './due_date_select';
case 'admin:groups:create':
BindInOut.initAll();
new Group();
- new GroupAvatar();
+ groupAvatar();
break;
case 'groups:edit':
case 'admin:groups:edit':
- new GroupAvatar();
+ groupAvatar();
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
@@ -473,7 +476,7 @@ import DueDateSelectors from './due_date_select';
const $el = $(el);
if ($el.find('.dropdown-group-label').length) {
- new gl.GroupLabelSubscription($el);
+ new GroupLabelSubscription($el);
} else {
new gl.ProjectLabelSubscription($el);
}
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index a00d29a845a..90020344748 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,6 +1,3 @@
-/* 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
* that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
@@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded';
export default {
init($diffFile) {
- /* Caching is used only when the following members are *true*. This is because there are likely to be
- * differently configured versions of diffs in the same session. However if these values are true, they
+ /* Caching is used only when the following members are *true*.
+ * 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 */
if (!this.userCanCreateNote) {
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index f03b47b1c1d..2168ff3a8ba 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,19 +1,12 @@
-/* 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 */
-
-window.GroupAvatar = (function() {
- function GroupAvatar() {
- $('.js-choose-group-avatar-button').on("click", function() {
- var form;
- form = $(this).closest("form");
- return form.find(".js-group-avatar-input").click();
- });
- $('.js-group-avatar-input').on("change", function() {
- var filename, form;
- form = $(this).closest("form");
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find(".js-avatar-filename").text(filename);
- });
- }
-
- return GroupAvatar;
-})();
+export default function groupAvatar() {
+ $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() {
+ const form = $(this).closest('form');
+ return form.find('.js-group-avatar-input').click();
+ });
+ $('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
+ const form = $(this).closest('form');
+ // eslint-disable-next-line no-useless-escape
+ const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+}
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 7dc9ce898e8..befaebb635e 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,6 +1,4 @@
-/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
-
-class GroupLabelSubscription {
+export default class GroupLabelSubscription {
constructor(container) {
const $container = $(container);
this.$dropdown = $container.find('.dropdown');
@@ -18,7 +16,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
- url: url
+ url,
}).done(() => {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
@@ -35,7 +33,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
- url: url
+ url,
}).done(() => {
this.toggleSubscriptionButtons();
});
@@ -47,6 +45,3 @@ class GroupLabelSubscription {
this.$unsubscribeButtons.toggleClass('hidden');
}
}
-
-window.gl = window.gl || {};
-window.gl.GroupLabelSubscription = GroupLabelSubscription;
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 90ca70289ab..a69a0bde17b 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,121 +1,86 @@
-/* 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 { 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() {
- function GroupsSelect() {
- $('.ajax-groups-select').each((function(_this) {
- const self = _this;
-
- 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);
+ 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,
- page,
- more,
+ pagination: {
+ more,
+ },
};
- },
- },
- initSelection: function(element, callback) {
- var id;
- id = $(element).val();
- if (id !== "") {
- return Api.group(id, callback);
- }
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return self.formatResult.apply(self, args);
- },
- 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;
- };
+ })
+ .then(params.success)
+ .fail(params.error);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- GroupsSelect.prototype.forceOverflow = function (e) {
- this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`;
- };
+ const groups = data.length ? data : data.results || [];
+ 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`;
+ });
+ });
+}
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index f785ed29e6c..1bab7965c19 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,7 +1,7 @@
import stickyMonitor from './lib/utils/sticky';
-export default () => {
- stickyMonitor(document.querySelector('.js-diff-files-changed'));
+export default (stickyTop) => {
+ stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({
filterable: true,
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 1d305f1eb2f..73791edaebb 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -51,20 +51,19 @@ const PARTICIPANTS_ROW_COUNT = 7;
}
IssuableContext.prototype.initParticipants = function() {
- $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
- return $(".js-participants-author").each(function(i) {
+ $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
+ return $('.js-participants-author').each(function(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
- return $(this).addClass("js-participants-hidden").hide();
+ return $(this).addClass('js-participants-hidden').hide();
}
});
};
- IssuableContext.prototype.toggleHiddenParticipants = function(e) {
- var currentText, lessText, originalText;
- e.preventDefault();
- currentText = $(this).text().trim();
- lessText = $(this).data("less-text");
- originalText = $(this).data("original-text");
+ IssuableContext.prototype.toggleHiddenParticipants = function() {
+ const currentText = $(this).text().trim();
+ const lessText = $(this).data('less-text');
+ const originalText = $(this).data('original-text');
+
if (currentText === originalText) {
$(this).text(lessText);
@@ -73,7 +72,7 @@ const PARTICIPANTS_ROW_COUNT = 7;
$(this).text(originalText);
}
- $(".js-participants-hidden").toggle();
+ $('.js-participants-hidden').toggle();
};
return IssuableContext;
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index d064a2c0024..a6f82b247e2 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,7 +1,7 @@
/* 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 Cookies from 'js-cookie';
-import NewNavSidebar from './new_sidebar';
+import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
(function() {
@@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav';
});
$(() => {
- const newNavSidebar = new NewNavSidebar();
- newNavSidebar.bindEvents();
+ const contextualSidebar = new ContextualSidebar();
+ contextualSidebar.bindEvents();
initFlyOutNav();
});
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 64db42701ce..098afcfa1b4 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -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;
- const computedStyle = window.getComputedStyle(el);
-
- if (!/sticky/.test(computedStyle.position)) return;
-
- const stickyTop = parseInt(computedStyle.top, 10);
+ if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true,
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 4cf07e99161..38403fdaf6e 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -50,16 +50,11 @@ import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
-import './diff';
-import './files_comment_button';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
-import './group_avatar';
-import './group_label_subscription';
-import './groups_select';
import './header';
import './importer_status';
import './issuable_index';
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index df042c7baff..54c1b7a268e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -11,8 +11,8 @@ import {
handleLocationHash,
isMetaClick,
} from './lib/utils/common_utils';
-
import initDiscussionTab from './image_diff/init_discussion_tab';
+import Diff from './diff';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -67,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
+ const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ const navbar = document.querySelector('.navbar-gitlab');
+ const paddingTop = 16;
+
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
@@ -76,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
+ this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
+
+ if (mergeRequestTabs) {
+ this.stickyTop += mergeRequestTabs.offsetHeight;
+ }
if (stubLocation) {
location = stubLocation;
@@ -278,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
const $container = $('#diffs');
$container.html(data.html);
- initChangesDropdown();
+ initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
@@ -292,7 +301,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
}
this.diffsLoaded = true;
- new gl.Diff();
+ new Diff();
this.scrollToElement('#diffs');
$('.diff-file').each((i, el) => {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9c008da1a5d..5a6868be444 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1280,10 +1280,12 @@ export default class Notes {
* Get data from Form attributes to use for saving/submitting comment.
*/
getFormData($form) {
+ const content = $form.find('.js-note-text').val();
return {
formData: $form.serialize(),
- formContent: _.escape($form.find('.js-note-text').val()),
+ formContent: _.escape(content),
formAction: $form.attr('action'),
+ formContentOriginal: content,
};
}
@@ -1415,7 +1417,7 @@ export default class Notes {
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
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 systemNoteUniqueId;
let hasQuickActions = false;
@@ -1574,7 +1576,7 @@ export default class Notes {
$form = $notesContainer.parent().find('form');
}
- $form.find('.js-note-text').val(formContent);
+ $form.find('.js-note-text').val(formContentOriginal);
this.reenableTargetFormSubmitButton(e);
this.addNoteError($form);
});
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
index baf43190d9e..0f13221b81e 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue';
- import placeholderNote from './issue_placeholder_note.vue';
- import placeholderSystemNote from './issue_placeholder_system_note.vue';
+ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
export default {
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue
index aecd1f957e5..5c9119644e3 100644
--- a/app/assets/javascripts/notes/components/issue_notes_app.vue
+++ b/app/assets/javascripts/notes/components/issue_notes_app.vue
@@ -5,10 +5,10 @@
import * as constants from '../constants';
import issueNote from './issue_note.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 placeholderNote from './issue_placeholder_note.vue';
- import placeholderSystemNote from './issue_placeholder_system_note.vue';
+ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
@@ -37,7 +37,7 @@
components: {
issueNote,
issueDiscussion,
- issueSystemNote,
+ systemNote,
issueCommentForm,
loadingIcon,
placeholderNote,
@@ -68,7 +68,7 @@
}
return placeholderNote;
} else if (note.individual_note) {
- return note.notes[0].system ? issueSystemNote : issueNote;
+ return note.notes[0].system ? systemNote : issueNote;
}
return issueDiscussion;
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 085bd20cefe..3da60e88474 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -12,6 +12,15 @@
type: Object,
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: {
tablePagination,
@@ -187,7 +196,7 @@
:empty-state-svg-path="emptyStateSvgPath"
/>
- <error-state
+ <error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/>
@@ -206,6 +215,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath"
+ :view-type="viewType"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 7aa0c0e8a7f..16a705cbaff 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -21,6 +21,10 @@
type: String,
required: true,
},
+ viewType: {
+ type: String,
+ required: true,
+ },
},
components: {
pipelinesTableRowComponent,
@@ -59,6 +63,7 @@
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
+ :view-type="viewType"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 5b9bb6c3750..33fbce993b2 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
+ viewType: {
+ type: String,
+ required: true,
+ },
},
components: {
asyncButtonComponent,
@@ -203,9 +207,13 @@ export default {
displayPipelineActions() {
return this.pipeline.flags.retryable ||
- this.pipeline.flags.cancelable ||
- this.pipeline.details.manual_actions.length ||
- this.pipeline.details.artifacts.length;
+ this.pipeline.flags.cancelable ||
+ this.pipeline.details.manual_actions.length ||
+ this.pipeline.details.artifacts.length;
+ },
+
+ isChildView() {
+ return this.viewType === 'child';
},
},
};
@@ -218,7 +226,10 @@ export default {
Status
</div>
<div class="table-mobile-content">
- <ci-badge :status="pipelineStatus"/>
+ <ci-badge
+ :status="pipelineStatus"
+ :show-text="!isChildView"
+ />
</div>
</div>
@@ -240,7 +251,9 @@ export default {
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
- :author="commitAuthor"/>
+ :author="commitAuthor"
+ :show-branch="!isChildView"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue
new file mode 100644
index 00000000000..eac43e692b0
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_branch_form.vue
@@ -0,0 +1,115 @@
+<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>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
new file mode 100644
index 00000000000..3ccb50213ab
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue
@@ -0,0 +1,86 @@
+<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>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
new file mode 100644
index 00000000000..5ef629e0dde
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
@@ -0,0 +1,90 @@
+<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>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index 0a89a9f16cb..788976a9804 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -8,7 +8,9 @@ import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
+import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
+import eventHub from '../event_hub';
export default {
data() {
@@ -24,12 +26,19 @@ export default {
PopupDialog,
RepoPreview,
},
-
+ created() {
+ eventHub.$on('createNewBranch', this.createNewBranch);
+ },
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
-
+ destroyed() {
+ eventHub.$off('createNewBranch', this.createNewBranch);
+ },
methods: {
+ getCurrentLocation() {
+ return location.href;
+ },
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
@@ -37,9 +46,30 @@ export default {
dialogSubmitted(status) {
this.toggleDialogOpen(false);
this.dialog.status = status;
- },
+ // remove tmp files
+ Helper.removeAllTmpFiles('openedFiles');
+ Helper.removeAllTmpFiles('files');
+ },
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>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 185cd90ac06..0d6259a37a8 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -49,7 +49,7 @@ export default {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
- action: 'update',
+ action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.newContent,
}));
@@ -62,7 +62,6 @@ export default {
if (newBranch) {
payload.start_branch = this.currentBranch;
}
- this.submitCommitsLoading = true;
Service.commitFiles(payload)
.then(() => {
this.resetCommitState();
@@ -78,6 +77,8 @@ export default {
},
tryCommit(e, skipBranchCheck = false, newBranch = false) {
+ this.submitCommitsLoading = true;
+
if (skipBranchCheck) {
this.makeCommit(newBranch);
} else {
@@ -90,6 +91,7 @@ export default {
this.makeCommit(newBranch);
})
.catch(() => {
+ this.submitCommitsLoading = false;
Flash('An error occurred while committing your changes');
});
}
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 4639bee6d66..df4caba51d8 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -16,7 +16,7 @@ const RepoEditor = {
},
mounted() {
- Service.getRaw(this.activeFile.raw_path)
+ Service.getRaw(this.activeFile)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data;
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index 03cd219e718..c98f641c853 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -11,7 +11,12 @@ const RepoFileButtons = {
mixins: [RepoMixin],
computed: {
-
+ showButtons() {
+ return this.activeFile.raw_path ||
+ this.activeFile.blame_path ||
+ this.activeFile.commits_path ||
+ this.activeFile.permalink;
+ },
rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw';
},
@@ -30,7 +35,10 @@ export default RepoFileButtons;
</script>
<template>
- <div id="repo-file-buttons">
+ <div
+ v-if="showButtons"
+ class="repo-file-buttons"
+ >
<a
:href="activeFile.raw_path"
target="_blank"
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index 098715915b0..405d7b4cf86 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -18,8 +18,8 @@ const RepoTab = {
},
changedClass() {
const tabChangedObj = {
- 'fa-times close-icon': !this.tab.changed,
- 'fa-circle unsaved-icon': this.tab.changed,
+ 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
+ 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
};
return tabChangedObj;
},
@@ -30,7 +30,7 @@ const RepoTab = {
Store.setActiveFiles(file);
},
closeTab(file) {
- if (file.changed) return;
+ if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file);
},
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
index f7b7f93e4b8..fb26f3b7380 100644
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -1,4 +1,3 @@
-import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import Flash from '../../flash';
@@ -8,6 +7,7 @@ const RepoHelper = {
getDefaultActiveFile() {
return {
+ id: '',
active: true,
binary: false,
extension: '',
@@ -62,6 +62,7 @@ const RepoHelper = {
});
RepoHelper.updateHistoryEntry(tree.url, title);
+ Store.path = tree.path;
},
setDirectoryToClosed(entry) {
@@ -96,8 +97,8 @@ const RepoHelper = {
.then((response) => {
const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
- if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
- Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
+ if (data.path && !Store.isInitialRoot) {
+ Store.isRoot = data.path === '/';
Store.isInitialRoot = Store.isRoot;
}
@@ -110,7 +111,7 @@ const RepoHelper = {
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
} else if (!Store.isPreviewView() && !data.render_error) {
- Service.getRaw(data.raw_path)
+ Service.getRaw(data)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data;
@@ -138,6 +139,10 @@ const RepoHelper = {
addToDirectory(file, data) {
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));
tree.files = files;
@@ -157,7 +162,18 @@ const RepoHelper = {
},
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 {
id,
@@ -165,11 +181,14 @@ const RepoHelper = {
name,
url,
tree_url,
+ path,
level,
+ tempFile,
icon: `fa-${icon}`,
files: [],
loading: false,
- opened: false,
+ opened,
+ active,
// eslint-disable-next-line camelcase
lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`,
@@ -213,7 +232,7 @@ const RepoHelper = {
},
findOpenedFileFromActive() {
- return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
+ return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
},
getFileFromPath(path) {
@@ -223,6 +242,76 @@ const RepoHelper = {
loadingError() {
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;
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
index 65dee7d5fd1..72fc5a70648 100644
--- a/app/assets/javascripts/repo/index.js
+++ b/app/assets/javascripts/repo/index.js
@@ -5,6 +5,8 @@ import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.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';
function initDropdowns() {
@@ -27,6 +29,7 @@ function setInitialStore(data) {
Store.service = Service;
Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl;
+ Store.path = data.currentPath;
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
@@ -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() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
+ const newDropdownHolder = document.querySelector('.js-new-dropdown');
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
@@ -73,6 +109,8 @@ function initRepoBundle() {
initRepo(repo);
initRepoEditButton(editButton);
+ initNewBranchForm();
+ initNewDropdown(newDropdownHolder);
}
$(initRepoBundle);
diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js
index c8e8238a0d3..efeda426b96 100644
--- a/app/assets/javascripts/repo/mixins/repo_mixin.js
+++ b/app/assets/javascripts/repo/mixins/repo_mixin.js
@@ -8,7 +8,7 @@ const RepoMixin = {
changedFiles() {
const changedFileList = this.openedFiles
- .filter(file => file.changed);
+ .filter(file => file.changed || file.tempFile);
return changedFileList;
},
},
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
index d68d71a4629..c9fa5cc8bf8 100644
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ b/app/assets/javascripts/repo/services/repo_service.js
@@ -1,8 +1,11 @@
import axios from 'axios';
+import csrf from '../../lib/utils/csrf';
import Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
+axios.defaults.headers.common[csrf.headerKey] = csrf.token;
+
const RepoService = {
url: '',
options: {
@@ -10,10 +13,17 @@ const RepoService = {
format: 'json',
},
},
+ createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/,
- getRaw(url) {
- return axios.get(url, {
+ getRaw(file) {
+ if (file.tempFile) {
+ return Promise.resolve({
+ data: '',
+ });
+ }
+
+ return axios.get(file.raw_path, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res],
});
@@ -73,6 +83,12 @@ const RepoService = {
.then(this.commitFlash);
},
+ createBranch(payload) {
+ const url = Api.buildUrl(this.createBranchPath)
+ .replace(':id', Store.projectId);
+ return axios.post(url, payload);
+ },
+
commitFlash(data) {
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');
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
index 49d7317a17e..38df1e3e0d2 100644
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ b/app/assets/javascripts/repo/stores/repo_store.js
@@ -13,6 +13,7 @@ const RepoStore = {
projectId: '',
projectName: '',
projectUrl: '',
+ branchUrl: '',
blobRaw: '',
currentBlobView: 'repo-preview',
openedFiles: [],
@@ -38,6 +39,7 @@ const RepoStore = {
newMrTemplateUrl: '',
branchChanged: false,
commitMessage: '',
+ path: '',
loading: {
tree: false,
blob: false,
@@ -76,21 +78,23 @@ const RepoStore = {
} else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain;
} else {
- Service.getRaw(file.raw_path)
+ Service.getRaw(file)
.then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).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.setActiveLine(-1);
},
setFileActivity(file, openedFile, i) {
const activeFile = openedFile;
- activeFile.active = file.url === activeFile.url;
+ activeFile.active = file.id === activeFile.id;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
@@ -98,7 +102,7 @@ const RepoStore = {
},
setActiveFile(activeFile, i) {
- RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
+ RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
RepoStore.activeFileIndex = i;
},
@@ -120,6 +124,11 @@ const RepoStore = {
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.
if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {};
@@ -169,7 +178,7 @@ const RepoStore = {
// getters
isActiveFile(file) {
- return file && file.url === RepoStore.activeFile.url;
+ return file && file.id === RepoStore.activeFile.id;
},
isPreviewView() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index b8a96b23012..be37dd87de9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -286,6 +286,7 @@ export default {
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
+ class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
@@ -311,8 +312,8 @@ export default {
</button>
</template>
<template v-else>
- <span class="bold">
- The pipeline for this merge request has not succeeded yet
+ <span class="bold js-resolve-mr-widget-items-message">
+ You can only merge once the items above are resolved
</span>
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index caa28bff6db..5b6c6e8d0b9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,52 +1,64 @@
<script>
-import ciIcon from './ci_icon.vue';
-/**
- * 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: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- */
+ 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.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
-export default {
- props: {
- status: {
- type: Object,
- required: true,
+ export default {
+ props: {
+ status: {
+ type: Object,
+ required: true,
+ },
+ showText: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
- },
-
- components: {
- ciIcon,
- },
-
- computed: {
- cssClass() {
- const className = this.status.group;
+ components: {
+ ciIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ cssClass() {
+ const className = this.status.group;
- return className ? `ci-status ci-${this.status.group}` : 'ci-status';
+ return className ? `ci-status ci-${className}` : 'ci-status';
+ },
},
- },
-};
+ };
</script>
<template>
<a
:href="status.details_path"
- :class="cssClass">
+ :class="cssClass"
+ v-tooltip
+ :title="!showText ? status.text : ''">
<ci-icon :status="status" />
- {{status.text}}
+
+ <template v-if="showText">
+ {{status.text}}
+ </template>
</a>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 50d14282cad..52814de8b2d 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -63,14 +63,17 @@
required: false,
default: () => ({}),
},
+ showBranch: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
- * TODO: Improve this! Use lodash _.has when we have it.
- *
* @returns {Boolean}
*/
hasCommitRef() {
@@ -80,8 +83,6 @@
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
- * TODO: Improve this! Use lodash _.has when we have it.
- *
* @returns {Boolean}
*/
hasAuthor() {
@@ -114,31 +115,30 @@
</script>
<template>
<div class="branch-commit">
- <div
- v-if="hasCommitRef"
- class="icon-container hidden-xs">
- <i
- v-if="tag"
- class="fa fa-tag"
- aria-hidden="true">
- </i>
- <i
- v-if="!tag"
- class="fa fa-code-fork"
- aria-hidden="true">
- </i>
- </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>
+ <template v-if="hasCommitRef && showBranch">
+ <div
+ class="icon-container hidden-xs">
+ <i
+ v-if="tag"
+ class="fa fa-tag"
+ aria-hidden="true">
+ </i>
+ <i
+ v-if="!tag"
+ class="fa fa-code-fork"
+ aria-hidden="true">
+ </i>
+ </div>
+ <a
+ class="ref-name hidden-xs"
+ :href="commitRef.ref_url"
+ v-tooltip
+ data-container="body"
+ :title="commitRef.name">
+ {{commitRef.name}}
+ </a>
+ </template>
<div
v-html="commitIconSvg"
class="commit-icon js-commit-icon">
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
new file mode 100644
index 00000000000..6670b554faf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -0,0 +1,71 @@
+<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>
diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 6921d91372f..e467ca56704 100644
--- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -1,9 +1,26 @@
<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 userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
- name: 'issuePlaceholderNote',
+ name: 'placeholderNote',
props: {
note: {
type: Object,
diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
index 80a8ef56a83..d805fea8006 100644
--- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
@@ -1,4 +1,12 @@
<script>
+ /**
+ * Common component to render a placeholder system note.
+ *
+ * @example
+ * <placeholder-system-note
+ * :note="{ body: 'Commands are being applied'}"
+ * />
+ */
export default {
name: 'placeholderSystemNote',
props: {
diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 0cfb6522e77..98f8f32557d 100644
--- a/app/assets/javascripts/notes/components/issue_system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,6 +1,24 @@
<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 issueNoteHeader from './issue_note_header.vue';
+ import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
+ import { spriteIcon } from '../../../lib/utils/common_utils';
export default {
name: 'systemNote',
@@ -24,7 +42,7 @@
return this.targetNoteHash === this.noteAnchorId;
},
iconHtml() {
- return gl.utils.spriteIcon(this.note.system_note_icon_name);
+ return spriteIcon(this.note.system_note_icon_name);
},
},
};
@@ -46,7 +64,8 @@
:author="note.author"
:created-at="note.created_at"
:note-id="note.id"
- :action-text-html="note.note_html" />
+ :action-text-html="note.note_html"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index 7d8c5936b7d..9e8c10bdc1a 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -9,7 +9,7 @@ export default {
},
text: {
type: String,
- required: true,
+ required: false,
},
kind: {
type: String,
@@ -82,14 +82,15 @@ export default {
type="button"
class="btn"
:class="btnCancelKindClass"
- @click="emitSubmit(false)">
- {{closeButtonLabel}}
+ @click="close">
+ {{ closeButtonLabel }}
</button>
- <button type="button"
+ <button
+ type="button"
class="btn"
:class="btnKindClass"
@click="emitSubmit(true)">
- {{primaryButtonLabel}}
+ {{ primaryButtonLabel }}
</button>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 95898d54cf7..dc32e783258 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -12,12 +12,14 @@
:img-alt="tooltipText"
:img-size="20"
:tooltip-text="tooltipText"
- tooltip-placement="top"
+ :tooltip-placement="top"
+ :username="username"
/>
*/
import userAvatarImage from './user_avatar_image.vue';
+import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -60,6 +62,22 @@ export default {
required: false,
default: 'top',
},
+ username: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldShowUsername() {
+ return this.username.length > 0;
+ },
+ avatarTooltipText() {
+ return this.shouldShowUsername ? '' : this.tooltipText;
+ },
+ },
+ directives: {
+ tooltip,
},
};
</script>
@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt"
:css-classes="imgCssClasses"
:size="imgSize"
- :tooltip-text="tooltipText"
+ :tooltip-text="avatarTooltipText"
+ :tooltip-placement="tooltipPlacement"
+ /><span
+ v-if="shouldShowUsername"
+ v-tooltip
+ :title="tooltipText"
:tooltip-placement="tooltipPlacement"
- />
+ >{{username}}</span>
</a>
</template>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index aa61ddc6a2c..7b1ef003bb2 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -5,6 +5,7 @@
@import "framework/layout";
@import "framework/animations";
+@import "framework/vue_transitions";
@import "framework/avatar";
@import "framework/asciidoctor";
@import "framework/banner";
@@ -36,7 +37,7 @@
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
-@import "framework/new-sidebar";
+@import "framework/contextual-sidebar";
@import "framework/tables";
@import "framework/notes";
@import "framework/tabs";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b131e2d57ee..00a0e9cef67 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -292,6 +292,11 @@
}
}
+.btn-align-content {
+ display: flex;
+ align-items: center;
+}
+
.btn-group {
&.btn-grouped {
@include btn-with-margin;
diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index 7a309f2c8a1..fa5d3833f3e 100644
--- a/app/assets/stylesheets/framework/new-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -1,24 +1,10 @@
-@import "framework/variables";
-@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 {
+.page-with-contextual-sidebar {
@media (min-width: $screen-md-min) {
- padding-left: $new-sidebar-collapsed-width;
+ padding-left: $contextual-sidebar-collapsed-width;
}
@media (min-width: $screen-lg-min) {
- padding-left: $new-sidebar-width;
+ padding-left: $contextual-sidebar-width;
}
// Override position: absolute
@@ -34,7 +20,7 @@ $new-sidebar-collapsed-width: 50px;
.page-with-icon-sidebar {
@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;
&:hover,
a:hover {
- background-color: $hover-background;
- color: $hover-color;
+ background-color: $link-hover-background;
+ color: $gl-text-color;
.settings-avatar {
svg {
- fill: $hover-color;
+ fill: $gl-text-color;
}
}
}
@@ -85,7 +71,7 @@ $new-sidebar-collapsed-width: 50px;
.nav-sidebar {
position: fixed;
z-index: 400;
- width: $new-sidebar-width;
+ width: $contextual-sidebar-width;
transition: left $sidebar-transition-duration;
top: $header-height;
bottom: 0;
@@ -103,7 +89,7 @@ $new-sidebar-collapsed-width: 50px;
&.sidebar-icons-only {
width: auto;
- min-width: $new-sidebar-collapsed-width;
+ min-width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -149,11 +135,11 @@ $new-sidebar-collapsed-width: 50px;
display: flex;
align-items: center;
padding: 12px 16px;
- color: $inactive-color;
+ color: $gl-text-color-secondary;
}
svg {
- fill: $inactive-color;
+ fill: $gl-text-color-secondary;
}
}
@@ -168,7 +154,7 @@ $new-sidebar-collapsed-width: 50px;
}
@media (max-width: $screen-xs-max) {
- left: (-$new-sidebar-width);
+ left: (-$contextual-sidebar-width);
}
.nav-icon-container {
@@ -210,8 +196,8 @@ $new-sidebar-collapsed-width: 50px;
&:hover,
&:focus {
- background: $active-hover-background;
- color: $active-hover-color;
+ background: $link-active-background;
+ color: $gl-text-color;
}
}
@@ -220,7 +206,7 @@ $new-sidebar-collapsed-width: 50px;
&,
&:hover,
&:focus {
- background: $active-background;
+ background: $link-active-background;
}
}
}
@@ -308,11 +294,11 @@ $new-sidebar-collapsed-width: 50px;
.badge {
background-color: $inactive-badge-background;
- color: $inactive-color;
+ color: $gl-text-color-secondary;
}
&.active {
- background: $active-background;
+ background: $link-active-background;
> a {
margin-left: 4px;
@@ -330,7 +316,7 @@ $new-sidebar-collapsed-width: 50px;
&.active > a:hover,
&.is-over > a {
- background-color: $hover-background;
+ background-color: $link-hover-background;
}
}
}
@@ -340,7 +326,7 @@ $new-sidebar-collapsed-width: 50px;
.toggle-sidebar-button,
.close-nav-button {
- width: $new-sidebar-width - 2px;
+ width: $contextual-sidebar-width - 2px;
position: fixed;
bottom: 0;
padding: 16px;
@@ -407,7 +393,7 @@ $new-sidebar-collapsed-width: 50px;
}
.toggle-sidebar-button {
- width: $new-sidebar-collapsed-width - 2px;
+ width: $contextual-sidebar-collapsed-width - 2px;
padding: 16px;
.collapse-text,
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b2847c348eb..0d80a85d521 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -65,7 +65,7 @@
display: flex;
flex: 1;
-webkit-flex: 1;
- padding-left: 30px;
+ padding-left: 12px;
position: relative;
margin-bottom: 0;
}
@@ -221,10 +221,6 @@
box-shadow: 0 0 4px $search-input-focus-shadow-color;
}
- &.focus .fa-filter {
- color: $common-gray-dark;
- }
-
gl-emoji {
display: inline-block;
font-family: inherit;
@@ -251,13 +247,6 @@
}
}
- .fa-filter {
- position: absolute;
- top: 10px;
- left: 10px;
- color: $gray-darkest;
- }
-
.fa-times {
right: 10px;
color: $gray-darkest;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d5ca23ff870..8ab48e4844f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -9,6 +9,8 @@ $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
+$contextual-sidebar-width: 220px;
+$contextual-sidebar-collapsed-width: 50px;
/*
* Color schema
@@ -359,6 +361,13 @@ $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09);
$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
*/
$btn-active-gray: #ececec;
@@ -404,7 +413,6 @@ $note-targe3-inside: #ffffd3;
$note-line2-border: #ddd;
$note-icon-gutter-width: 55px;
-
/*
* Zen
*/
diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss
new file mode 100644
index 00000000000..e07a177e153
--- /dev/null
+++ b/app/assets/stylesheets/framework/vue_transitions.scss
@@ -0,0 +1,9 @@
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity $sidebar-transition-duration $general-hover-transition-curve;
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ca61f7a30c3..91296b354a7 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -414,7 +414,7 @@
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 {
position: relative;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 04b132415eb..f0cad30f4f3 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -249,13 +249,12 @@
width: 100%;
padding-right: 5px;
}
-
}
.discussion-actions {
display: table;
- .new-issue-for-discussion path {
+ .btn-default path {
fill: $gray-darkest;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index ebad429c2ba..3bd0e3ad535 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -466,6 +466,10 @@ ul.notes {
float: right;
margin-left: 10px;
color: $gray-darkest;
+
+ .btn-group > .discussion-next-btn {
+ margin-left: -1px;
+ }
}
.note-actions {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index ea37ccf5e3d..6a363b1710e 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,8 +1,3 @@
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity $sidebar-transition-duration;
-}
-
.monaco-loader {
position: absolute;
top: 0;
@@ -206,7 +201,7 @@
}
}
- #repo-file-buttons {
+ .repo-file-buttons {
background-color: $white-light;
padding: 5px 10px;
border-top: 1px solid $white-normal;
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 572915a4930..38f379dbf4f 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -57,6 +57,10 @@ class HelpController < ApplicationController
def shortcuts
end
+ def instance_configuration
+ @instance_configuration = InstanceConfiguration.new
+ end
+
def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 183a6f88a6a..770381472c5 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
tree_path = path_segments.join('/')
render json: json.merge(
+ id: @blob.id,
path: blob.path,
name: blob.name,
extension: blob.extension,
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 7f03ce07dec..f28df83d5a5 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
@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
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b7a108a0ebd..fe1334c0cfe 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create]
# 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
before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
+ def edit
+ respond_with(@issue)
+ end
+
def show
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
@@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format|
+ format.html do
+ recaptcha_check_with_fallback { render :edit }
+ end
+
format.json do
render_issue_json
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 756f7e5df8c..f3719059f88 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
format.json do
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
Gitlab::GitalyClient.allow_n_plus_1_calls do
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index 533076585c0..852eac3647d 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -23,7 +23,7 @@ class BranchesFinder
def filter_by_name(branches)
if search
- branches.select { |branch| branch.name.include?(search) }
+ branches.select { |branch| branch.name.upcase.include?(search.upcase) }
else
branches
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 1ee8911bb1a..cd1ecaadb85 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -120,6 +120,15 @@ module ApplicationSettingsHelper
message.html_safe
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
_("When access to a storage fails. GitLab will prevent access to the "\
"storage for the time specified here. This allows the filesystem to "\
@@ -144,6 +153,8 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:auto_devops_enabled,
+ :circuitbreaker_access_retries,
+ :circuitbreaker_backoff_threshold,
:circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_reset_time,
:circuitbreaker_failure_wait_time,
diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb
new file mode 100644
index 00000000000..cee319f20bc
--- /dev/null
+++ b/app/helpers/instance_configuration_helper.rb
@@ -0,0 +1,18 @@
+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
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a23a43c9f43..5a74511afa7 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,7 +1,7 @@
module NavHelper
def page_with_sidebar_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
diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb
index 544c9efb845..4d2180f7eee 100644
--- a/app/helpers/storage_health_helper.rb
+++ b/app/helpers/storage_health_helper.rb
@@ -16,17 +16,16 @@ module StorageHealthHelper
def message_for_circuit_breaker(circuit_breaker)
maximum_failures = circuit_breaker.failure_count_threshold
current_failures = circuit_breaker.failure_count
- permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures
translation_params = { number_of_failures: current_failures,
maximum_failures: maximum_failures,
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 "\
"retry automatically. Reset storage information when the problem is "\
"resolved.") % translation_params
- elsif circuit_breaker.circuit_broken?
+ elsif circuit_breaker.backing_off?
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"block access for %{number_of_seconds} seconds.") % translation_params
else
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 4dda276bb41..f266e7db6da 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -153,13 +153,25 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
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_reset_time,
:circuitbreaker_storage_timeout,
presence: true,
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|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index cf3ce3c9e54..ca65e81f27a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -249,9 +249,7 @@ module Ci
end
def commit
- @commit ||= project.commit(sha)
- rescue
- nil
+ @commit ||= project.commit_by(oid: sha)
end
def branch?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index b6868ccbe8f..e613d21add6 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
- "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}"
+ "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}"
end
def formatted_external_url
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
new file mode 100644
index 00000000000..b30b707e5fe
--- /dev/null
+++ b/app/models/instance_configuration.rb
@@ -0,0 +1,71 @@
+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
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 5d798247863..2e824cda525 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
- after_create :update
- after_save :update
- after_destroy :update
+ after_create :update_daemon
+ after_save :update_daemon
+ after_destroy :update_daemon
def to_param
domain
@@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base
private
- def update
+ def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 4689b588906..7185b4d44fc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -540,6 +540,10 @@ class Project < ActiveRecord::Base
repository.commit(ref)
end
+ def commit_by(oid:)
+ repository.commit_by(oid: oid)
+ end
+
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref)
@@ -553,7 +557,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(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
def saved?
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 9ee3a533c1e..b487378edd2 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated?
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
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 8ba07173c74..5c0b3338a62 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService
end
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
def build_kubeclient!(api_path: 'api', api_version: 'v1')
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 4324ea46aac..44a1e9ce529 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -76,6 +76,7 @@ class Repository
@full_path = full_path
@disk_path = disk_path || full_path
@project = project
+ @commit_cache = {}
end
def ==(other)
@@ -103,18 +104,17 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
+ return ref if ref.is_a?(::Commit)
- commit =
- if ref.is_a?(Gitlab::Git::Commit)
- ref
- else
- Gitlab::Git::Commit.find(raw_repository, ref)
- end
+ find_commit(ref)
+ end
- commit = ::Commit.new(commit, @project) if commit
- commit
- rescue Rugged::OdbError, Rugged::TreeError
- nil
+ # Finding a commit by the passed SHA
+ # Also takes care of caching, based on the SHA
+ def commit_by(oid:)
+ return @commit_cache[oid] if @commit_cache.key?(oid)
+
+ @commit_cache[oid] = find_commit(oid)
end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
@@ -231,7 +231,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
- return unless sha && commit(sha)
+ return unless sha && commit_by(oid: sha)
return if kept_around?(sha)
@@ -862,22 +862,12 @@ class Repository
end
def ff_merge(user, source, target_branch, merge_request: nil)
- our_commit = rugged.branches[target_branch].target
- their_commit =
- 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?
+ their_commit_id = commit(source)&.id
+ raise 'Invalid merge source' if their_commit_id.nil?
- with_branch(user, target_branch) do |start_commit|
- merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
+ merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
- their_commit.oid
- end
+ with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
def revert(
@@ -912,18 +902,27 @@ class Repository
end
end
- def merged_to_root_ref?(branch_name)
- branch_commit = commit(branch_name)
- root_ref_commit = commit(root_ref)
+ def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
+ branch = Gitlab::Git::Branch.find(self, branch_or_name)
- if branch_commit
- same_head = branch_commit.id == root_ref_commit.id
- !same_head && ancestor?(branch_commit.id, root_ref_commit.id)
+ if branch
+ root_ref_sha = commit(root_ref).sha
+ 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
nil
end
end
+ delegate :merged_branch_names, to: :raw_repository
+
def merge_base(first_commit_id, second_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
@@ -1031,6 +1030,10 @@ class Repository
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
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
value =
if memoize_only
@@ -1040,8 +1043,9 @@ class Repository
end
instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
- # if e.g. HEAD or the entire repository doesn't exist we want to
- # gracefully handle this and not cache anything.
+ # Even if the above `#exists?` check passes these errors might still
+ # occur (for example because of a non-existing HEAD). We want to
+ # gracefully handle this and not cache anything
fallback
end
end
@@ -1069,6 +1073,18 @@ class Repository
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)
blob = blob_at(sha, path)
return unless blob
@@ -1107,12 +1123,12 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
- commit(c)
+ commit_by(oid: c)
end
def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path)
- commit(sha)
+ commit_by(oid: sha)
end
def last_commit_id_for_path_by_shelling_out(sha, path)
diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb
index f2bfb60604f..57e446d7f30 100644
--- a/app/services/users/last_push_event_service.rb
+++ b/app/services/users/last_push_event_service.rb
@@ -16,8 +16,8 @@ module Users
user_cache_key
]
- if event.project.forked?
- keys << project_cache_key(event.project.forked_from_project)
+ if forked_from = event.project.forked_from_project
+ keys << project_cache_key(forked_from)
end
keys.each { |key| set_key(key, event.id) }
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 2b23af9212e..3a4d5ce0b5c 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -533,11 +533,23 @@
%fieldset
%legend Git Storage Circuitbreaker settings
.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
- = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
+ = f.number_field :circuitbreaker_access_retries, class: 'form-control'
.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
= f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2'
.col-sm-10
@@ -545,17 +557,17 @@
.help-block
= circuitbreaker_failure_wait_time_help_text
.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
- = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
+ = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
.help-block
- = circuitbreaker_failure_reset_time_help_text
+ = circuitbreaker_failure_count_help_text
.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
- = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
+ = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
.help-block
- = circuitbreaker_storage_timeout_help_text
+ = circuitbreaker_failure_reset_time_help_text
%fieldset
%legend Repository Checks
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index fd2ba9ac1ca..9038c4fbebd 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -6,13 +6,13 @@
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs
= 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
= 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
= 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
.nav-controls
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index c25eae63eec..d0c2e0b1d69 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -11,6 +11,7 @@
%span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
= version_status_badge
+
%p.slead
GitLab is open source software to collaborate on code.
%br
@@ -23,6 +24,7 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
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
.row.prepend-top-default
diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml
new file mode 100644
index 00000000000..f09e3825a4b
--- /dev/null
+++ b/app/views/help/instance_configuration.html.haml
@@ -0,0 +1,17 @@
+- 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
diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml
new file mode 100644
index 00000000000..7fa8bd086d4
--- /dev/null
+++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml
@@ -0,0 +1,24 @@
+- 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])
diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml
new file mode 100644
index 00000000000..bdd77730dcc
--- /dev/null
+++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml
@@ -0,0 +1,35 @@
+- 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') }}
diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml
new file mode 100644
index 00000000000..987cc61b3f6
--- /dev/null
+++ b/app/views/help/instance_configuration/_ssh_info.html.haml
@@ -0,0 +1,27 @@
+- 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])
diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml
new file mode 100644
index 00000000000..a7d040d6821
--- /dev/null
+++ b/app/views/peek/views/_gitaly.html.haml
@@ -0,0 +1,7 @@
+- local_assigns.fetch(:view)
+
+%strong
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } } ...
+ \/
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } } ...
+ Gitaly
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 49101d1efa4..6e02ae6c9cc 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,3 +1,4 @@
+- merged = local_assigns.fetch(:merged, false)
- commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
@@ -12,7 +13,7 @@
&nbsp;
- if branch.name == @repository.root_ref
%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 } }
= s_('Branches|merged')
@@ -47,7 +48,7 @@
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, 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")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7d9645d79e6..aade310236e 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -38,7 +38,7 @@
- if @branches.any?
%ul.content-list.all-branches
- @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'
- else
.nothing-here-block
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index ff76abc3553..b127e06030e 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -47,7 +47,7 @@
- if can?(current_user, :update_cluster, @cluster)
.form-group
- = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success'
+ = field.submit _('Save'), class: 'btn btn-success'
%section.settings#js-cluster-details
.settings-header
@@ -68,7 +68,7 @@
%section.settings#js-cluster-advanced-settings
.settings-header
- %h4= s_('ClusterIntegration|Advanced settings')
+ %h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 09bcd187e59..ff17372fdd9 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -77,5 +77,6 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
- in
- = time_interval_in_words last_pipeline.duration
+ - if last_pipeline.duration
+ in
+ = time_interval_in_words last_pipeline.duration
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index c9956183e12..af564b93dc3 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -72,6 +72,7 @@
%pre.light-well
:preserve
cd existing_repo
+ git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git push -u origin --all
git push -u origin --tags
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
new file mode 100644
index 00000000000..1b7d878c38c
--- /dev/null
+++ b/app/views/projects/issues/edit.html.haml
@@ -0,0 +1,7 @@
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
+
+%h3.page-title
+ Edit Issue ##{@issue.iid}
+%hr
+
+= render "form"
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 6cddc38d11a..7ea19e6c828 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,8 +1,10 @@
.tree-ref-container
.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'
.tree-controls
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 7ad743b3b81..6d7c9633913 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,3 +1,4 @@
+- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
@@ -7,8 +8,20 @@
= hidden_field_tag key, value, id: nil
.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_title _("Switch branch/tag")
- = dropdown_filter _("Search branches and tags")
- = dropdown_content
- = dropdown_loading
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ .dropdown-page-one
+ = dropdown_title _("Switch branch/tag")
+ = dropdown_filter _("Search branches and tags")
+ = 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
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index d2b62557e03..3f553c9fede 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -14,5 +14,5 @@
= link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- if participants_extra > 0
.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
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 161b1c9fd72..fabb17c7340 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -25,7 +25,6 @@
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) }
- = icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 7185f5bcc5b..7861f92b33f 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -7,4 +7,5 @@
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}}' }),
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 } }