Merge remote-tracking branch 'origin/master' into 35616-move-k8-to-cluster-page
This commit is contained in:
commit
3138fcdcd7
|
@ -20,6 +20,7 @@ class ListIssue {
|
|||
this.isFetching = {
|
||||
subscriptions: true,
|
||||
};
|
||||
this.isLoading = {};
|
||||
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
|
||||
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
|
||||
|
||||
|
@ -86,6 +87,10 @@ class ListIssue {
|
|||
this.isFetching[key] = value;
|
||||
}
|
||||
|
||||
setLoadingState(key, value) {
|
||||
this.isLoading[key] = value;
|
||||
}
|
||||
|
||||
update (url) {
|
||||
const data = {
|
||||
issue: {
|
||||
|
|
|
@ -514,10 +514,11 @@ GitLabDropdown = (function() {
|
|||
|
||||
const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
|
||||
const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
|
||||
const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
|
||||
const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
|
||||
|
||||
// Makes indeterminate items effective
|
||||
if (this.fullData && hasFilterBulkUpdate) {
|
||||
if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
|
||||
this.parseData(this.fullData);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import Cookies from 'js-cookie';
|
|||
|
||||
Sidebar.prototype.removeListeners = function () {
|
||||
this.sidebar.off('click', '.sidebar-collapsed-icon');
|
||||
$('.dropdown').off('hidden.gl.dropdown');
|
||||
this.sidebar.off('hidden.gl.dropdown');
|
||||
$('.dropdown').off('loading.gl.dropdown');
|
||||
$('.dropdown').off('loaded.gl.dropdown');
|
||||
$(document).off('click', '.js-sidebar-toggle');
|
||||
|
@ -25,7 +25,7 @@ import Cookies from 'js-cookie';
|
|||
const $document = $(document);
|
||||
|
||||
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
|
||||
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
|
||||
this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
|
||||
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
|
||||
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
|
||||
|
||||
|
@ -180,7 +180,7 @@ import Cookies from 'js-cookie';
|
|||
var $block, sidebar;
|
||||
sidebar = e.data;
|
||||
e.preventDefault();
|
||||
$block = $(this).closest('.block');
|
||||
$block = $(e.target).closest('.block');
|
||||
return sidebar.sidebarDropdownHidden($block);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import Vue from 'vue';
|
||||
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees';
|
||||
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
|
||||
import SidebarMoveIssue from './lib/sidebar_move_issue';
|
||||
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
|
||||
import sidebarParticipants from './components/participants/sidebar_participants.vue';
|
||||
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
function mountConfidentialComponent(mediator) {
|
||||
const el = document.getElementById('js-confidential-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const dataNode = document.getElementById('js-confidential-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
|
||||
|
||||
new ConfidentialComp({
|
||||
propsData: {
|
||||
isConfidential: initialData.is_confidential,
|
||||
isEditable: initialData.is_editable,
|
||||
service: mediator.service,
|
||||
},
|
||||
}).$mount(el);
|
||||
}
|
||||
|
||||
function mountLockComponent(mediator) {
|
||||
const el = document.getElementById('js-lock-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const dataNode = document.getElementById('js-lock-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
const LockComp = Vue.extend(LockIssueSidebar);
|
||||
|
||||
new LockComp({
|
||||
propsData: {
|
||||
isLocked: initialData.is_locked,
|
||||
isEditable: initialData.is_editable,
|
||||
mediator,
|
||||
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
|
||||
},
|
||||
}).$mount(el);
|
||||
}
|
||||
|
||||
function mountParticipantsComponent() {
|
||||
const el = document.querySelector('.js-sidebar-participants-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
sidebarParticipants,
|
||||
},
|
||||
render: createElement => createElement('sidebar-participants', {}),
|
||||
});
|
||||
}
|
||||
|
||||
function mountSubscriptionsComponent() {
|
||||
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
sidebarSubscriptions,
|
||||
},
|
||||
render: createElement => createElement('sidebar-subscriptions', {}),
|
||||
});
|
||||
}
|
||||
|
||||
function mount(mediator) {
|
||||
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
|
||||
// Only create the sidebarAssignees vue app if it is found in the DOM
|
||||
// We currently do not use sidebarAssignees for the MR page
|
||||
if (sidebarAssigneesEl) {
|
||||
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
|
||||
}
|
||||
|
||||
mountConfidentialComponent(mediator);
|
||||
mountLockComponent(mediator);
|
||||
mountParticipantsComponent();
|
||||
mountSubscriptionsComponent();
|
||||
|
||||
new SidebarMoveIssue(
|
||||
mediator,
|
||||
$('.js-move-issue'),
|
||||
$('.js-move-issue-confirmation-button'),
|
||||
).init();
|
||||
|
||||
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
|
||||
}
|
||||
|
||||
export default mount;
|
|
@ -1,110 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees';
|
||||
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
|
||||
import SidebarMoveIssue from './lib/sidebar_move_issue';
|
||||
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
|
||||
import sidebarParticipants from './components/participants/sidebar_participants.vue';
|
||||
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
||||
import Mediator from './sidebar_mediator';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
function mountConfidentialComponent(mediator) {
|
||||
const el = document.getElementById('js-confidential-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const dataNode = document.getElementById('js-confidential-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
|
||||
|
||||
new ConfidentialComp({
|
||||
propsData: {
|
||||
isConfidential: initialData.is_confidential,
|
||||
isEditable: initialData.is_editable,
|
||||
service: mediator.service,
|
||||
},
|
||||
}).$mount(el);
|
||||
}
|
||||
|
||||
function mountLockComponent(mediator) {
|
||||
const el = document.getElementById('js-lock-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const dataNode = document.getElementById('js-lock-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
const LockComp = Vue.extend(LockIssueSidebar);
|
||||
|
||||
new LockComp({
|
||||
propsData: {
|
||||
isLocked: initialData.is_locked,
|
||||
isEditable: initialData.is_editable,
|
||||
mediator,
|
||||
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
|
||||
},
|
||||
}).$mount(el);
|
||||
}
|
||||
|
||||
function mountParticipantsComponent() {
|
||||
const el = document.querySelector('.js-sidebar-participants-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
sidebarParticipants,
|
||||
},
|
||||
render: createElement => createElement('sidebar-participants', {}),
|
||||
});
|
||||
}
|
||||
|
||||
function mountSubscriptionsComponent() {
|
||||
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
|
||||
|
||||
if (!el) return;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
sidebarSubscriptions,
|
||||
},
|
||||
render: createElement => createElement('sidebar-subscriptions', {}),
|
||||
});
|
||||
}
|
||||
import mountSidebar from './mount_sidebar';
|
||||
|
||||
function domContentLoaded() {
|
||||
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
|
||||
const mediator = new Mediator(sidebarOptions);
|
||||
mediator.fetch();
|
||||
|
||||
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
|
||||
// Only create the sidebarAssignees vue app if it is found in the DOM
|
||||
// We currently do not use sidebarAssignees for the MR page
|
||||
if (sidebarAssigneesEl) {
|
||||
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
|
||||
}
|
||||
|
||||
mountConfidentialComponent(mediator);
|
||||
mountLockComponent(mediator);
|
||||
mountParticipantsComponent();
|
||||
mountSubscriptionsComponent();
|
||||
|
||||
new SidebarMoveIssue(
|
||||
mediator,
|
||||
$('.js-move-issue'),
|
||||
$('.js-move-issue-confirmation-button'),
|
||||
).init();
|
||||
|
||||
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
|
||||
mountSidebar(mediator);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', domContentLoaded);
|
||||
|
|
|
@ -5,19 +5,23 @@ import Store from './stores/sidebar_store';
|
|||
export default class SidebarMediator {
|
||||
constructor(options) {
|
||||
if (!SidebarMediator.singleton) {
|
||||
this.store = new Store(options);
|
||||
this.service = new Service({
|
||||
endpoint: options.endpoint,
|
||||
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
|
||||
moveIssueEndpoint: options.moveIssueEndpoint,
|
||||
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
|
||||
});
|
||||
SidebarMediator.singleton = this;
|
||||
this.initSingleton(options);
|
||||
}
|
||||
|
||||
return SidebarMediator.singleton;
|
||||
}
|
||||
|
||||
initSingleton(options) {
|
||||
this.store = new Store(options);
|
||||
this.service = new Service({
|
||||
endpoint: options.endpoint,
|
||||
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
|
||||
moveIssueEndpoint: options.moveIssueEndpoint,
|
||||
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
|
||||
});
|
||||
SidebarMediator.singleton = this;
|
||||
}
|
||||
|
||||
assignYourself() {
|
||||
this.store.addAssignee(this.store.currentUser);
|
||||
}
|
||||
|
@ -35,17 +39,21 @@ export default class SidebarMediator {
|
|||
}
|
||||
|
||||
fetch() {
|
||||
this.service.get()
|
||||
return this.service.get()
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
this.store.setAssigneeData(data);
|
||||
this.store.setTimeTrackingData(data);
|
||||
this.store.setParticipantsData(data);
|
||||
this.store.setSubscriptionsData(data);
|
||||
this.processFetchedData(data);
|
||||
})
|
||||
.catch(() => new Flash('Error occurred when fetching sidebar data'));
|
||||
}
|
||||
|
||||
processFetchedData(data) {
|
||||
this.store.setAssigneeData(data);
|
||||
this.store.setTimeTrackingData(data);
|
||||
this.store.setParticipantsData(data);
|
||||
this.store.setSubscriptionsData(data);
|
||||
}
|
||||
|
||||
toggleSubscription() {
|
||||
this.store.setFetchingState('subscriptions', true);
|
||||
return this.service.toggleSubscription()
|
||||
|
|
|
@ -15,6 +15,7 @@ export default class SidebarStore {
|
|||
participants: true,
|
||||
subscriptions: true,
|
||||
};
|
||||
this.isLoading = {};
|
||||
this.autocompleteProjects = [];
|
||||
this.moveToProjectId = 0;
|
||||
this.isLockDialogOpen = false;
|
||||
|
@ -55,6 +56,10 @@ export default class SidebarStore {
|
|||
this.isFetching[key] = value;
|
||||
}
|
||||
|
||||
setLoadingState(key, value) {
|
||||
this.isLoading[key] = value;
|
||||
}
|
||||
|
||||
addAssignee(assignee) {
|
||||
if (!this.findAssignee(assignee)) {
|
||||
this.assignees.push(assignee);
|
||||
|
|
|
@ -139,7 +139,17 @@ class Namespace < ActiveRecord::Base
|
|||
def find_fork_of(project)
|
||||
return nil unless project.fork_network
|
||||
|
||||
project.fork_network.find_forks_in(projects).first
|
||||
if RequestStore.active?
|
||||
forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do
|
||||
Hash.new do |found_forks, project|
|
||||
found_forks[project] = project.fork_network.find_forks_in(projects).first
|
||||
end
|
||||
end
|
||||
|
||||
forks_in_namespace[project]
|
||||
else
|
||||
project.fork_network.find_forks_in(projects).first
|
||||
end
|
||||
end
|
||||
|
||||
def lfs_enabled?
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= render 'devise/shared/tab_single', tab_title:'Change your password'
|
||||
.login-box
|
||||
.login-body
|
||||
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
|
||||
= form_for(resource, as: resource_name, url: password_path(:user), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
= f.hidden_field :reset_password_token
|
||||
|
@ -17,5 +17,5 @@
|
|||
.clearfix.prepend-top-20
|
||||
%p
|
||||
%span.light Didn't receive a confirmation email?
|
||||
= link_to "Request a new one", new_confirmation_path(resource_name)
|
||||
= link_to "Request a new one", new_confirmation_path(:user)
|
||||
= render 'devise/shared/sign_in_link'
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
= f.check_box :remember_me, class: 'remember-me-checkbox'
|
||||
%span Remember me
|
||||
.pull-right.forgot-password
|
||||
= link_to "Forgot your password?", new_password_path(resource_name)
|
||||
= link_to "Forgot your password?", new_password_path(:user)
|
||||
.submit-container.move-submit-down
|
||||
= f.submit "Sign in", class: "btn btn-save"
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<%- if controller_name != 'sessions' %>
|
||||
<%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br />
|
||||
<%= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
|
||||
<% end -%>
|
||||
|
||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
|
||||
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
|
||||
<%= link_to "Sign up", new_registration_path(:user) %><br />
|
||||
<% end -%>
|
||||
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
|
||||
<%= link_to "Forgot your password?", new_password_path(resource_name), class: "btn" %><br />
|
||||
<%= link_to "Forgot your password?", new_password_path(:user), class: "btn" %><br />
|
||||
<% end -%>
|
||||
|
||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
|
||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(:user) %><br />
|
||||
<% end -%>
|
||||
|
||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(:user) %><br />
|
||||
<% end -%>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%p
|
||||
%span.light
|
||||
Already have login and password?
|
||||
= link_to "Sign in", new_session_path(resource_name)
|
||||
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes')
|
||||
|
|
|
@ -31,4 +31,4 @@
|
|||
%p
|
||||
%span.light Didn't receive a confirmation email?
|
||||
= succeed '.' do
|
||||
= link_to "Request a new one", new_confirmation_path(resource_name)
|
||||
= link_to "Request a new one", new_confirmation_path(:user)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
%p Try logging in using your username or email. If you have forgotten your password, try recovering it
|
||||
|
||||
= link_to "Sign in", new_session_path(:user), class: 'btn primary'
|
||||
= link_to "Recover password", new_password_path(resource_name), class: 'btn secondary'
|
||||
= link_to "Recover password", new_password_path(:user), class: 'btn secondary'
|
||||
|
||||
%hr
|
||||
%p.light If none of the options work, try contacting a GitLab administrator.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
- type = local_assigns.fetch(:type, :issues)
|
||||
- page_context_word = type.to_s.humanize(capitalize: false)
|
||||
- issuables = @issues || @merge_requests
|
||||
|
||||
%ul.nav-links.issues-state-filters
|
||||
%li{ class: active_when(params[:state] == 'opened') }>
|
||||
|
@ -20,6 +19,4 @@
|
|||
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
|
||||
#{issuables_state_counter_text(type, :closed)}
|
||||
|
||||
%li{ class: active_when(params[:state] == 'all') }>
|
||||
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
|
||||
#{issuables_state_counter_text(type, :all)}
|
||||
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
- page_context_word = local_assigns.fetch(:page_context_word)
|
||||
- counter = local_assigns.fetch(:counter)
|
||||
|
||||
%li{ class: active_when(params[:state] == 'all') }>
|
||||
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
|
||||
#{counter}
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Confirming email with invalid token should no longer generate an error
|
||||
merge_request: 15726
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reduce requests for project forks on show page of projects that have forks
|
||||
merge_request: 15663
|
||||
author:
|
||||
type: performance
|
|
@ -152,12 +152,23 @@ CE and EE.
|
|||
## Previewing the changes live
|
||||
|
||||
If you want to preview the doc changes of your merge request live, you can use
|
||||
the manual `review-docs-deploy` job in your merge request.
|
||||
the manual `review-docs-deploy` job in your merge request. You will need at
|
||||
least Master permissions to be able to run it and is currently enabled for the
|
||||
following projects:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce
|
||||
- https://gitlab.com/gitlab-org/gitlab-ee
|
||||
|
||||
NOTE: **Note:**
|
||||
You will need to push a branch to those repositories, it doesn't work for forks.
|
||||
|
||||
TIP: **Tip:**
|
||||
If your branch contains only documentation changes, you can use
|
||||
[special branch names](#testing) to avoid long running pipelines.
|
||||
|
||||
In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
|
||||
reveal the `review-docs-deploy` job. Hit the play button for the job to start.
|
||||
|
||||
![Manual trigger a docs build](img/manual_build_docs.png)
|
||||
|
||||
This job will:
|
||||
|
|
|
@ -2,7 +2,7 @@ module Gitlab
|
|||
module Git
|
||||
module Conflict
|
||||
class File
|
||||
attr_reader :content, :their_path, :our_path, :our_mode, :repository
|
||||
attr_reader :content, :their_path, :our_path, :our_mode, :repository, :commit_oid
|
||||
|
||||
def initialize(repository, commit_oid, conflict, content)
|
||||
@repository = repository
|
||||
|
|
|
@ -75,7 +75,7 @@ module Gitlab
|
|||
resolved_lines = file.resolve_lines(params[:sections])
|
||||
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
|
||||
|
||||
new_file << "\n" if file.our_blob.data.ends_with?("\n")
|
||||
new_file << "\n" if file.our_blob.data.end_with?("\n")
|
||||
elsif params[:content]
|
||||
new_file = file.resolve_content(params[:content])
|
||||
end
|
||||
|
|
|
@ -18,6 +18,8 @@ module Gitlab
|
|||
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
|
||||
].freeze
|
||||
SEARCH_CONTEXT_LINES = 3
|
||||
REBASE_WORKTREE_PREFIX = 'rebase'.freeze
|
||||
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
|
||||
|
||||
NoRepository = Class.new(StandardError)
|
||||
InvalidBlobName = Class.new(StandardError)
|
||||
|
@ -1070,13 +1072,8 @@ module Gitlab
|
|||
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
|
||||
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
|
||||
|
||||
command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z]
|
||||
input = "update #{ref_path}\x00#{ref}\x00\x00"
|
||||
output, status = circuit_breaker.perform do
|
||||
popen(command, path) { |stdin| stdin.write(input) }
|
||||
end
|
||||
|
||||
raise GitError, output unless status.zero?
|
||||
run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) }
|
||||
end
|
||||
|
||||
def fetch_ref(source_repository, source_ref:, target_ref:)
|
||||
|
@ -1098,14 +1095,22 @@ module Gitlab
|
|||
end
|
||||
|
||||
# Refactoring aid; allows us to copy code from app/models/repository.rb
|
||||
def run_git(args, env: {}, nice: false)
|
||||
def run_git(args, chdir: path, env: {}, nice: false, &block)
|
||||
cmd = [Gitlab.config.git.bin_path, *args]
|
||||
cmd.unshift("nice") if nice
|
||||
circuit_breaker.perform do
|
||||
popen(cmd, path, env)
|
||||
popen(cmd, chdir, env, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def run_git!(args, chdir: path, env: {}, nice: false, &block)
|
||||
output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
|
||||
|
||||
raise GitError, output unless status.zero?
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
# Refactoring aid; allows us to copy code from app/models/repository.rb
|
||||
def run_git_with_timeout(args, timeout, env: {})
|
||||
circuit_breaker.perform do
|
||||
|
@ -1175,6 +1180,64 @@ module Gitlab
|
|||
raise GitError.new("Could not fsck repository:\n#{output}") unless status.zero?
|
||||
end
|
||||
|
||||
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
|
||||
rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id)
|
||||
env = git_env_for_user(user)
|
||||
|
||||
with_worktree(rebase_path, branch, env: env) do
|
||||
run_git!(
|
||||
%W(pull --rebase #{remote_repository.path} #{remote_branch}),
|
||||
chdir: rebase_path, env: env
|
||||
)
|
||||
|
||||
rebase_sha = run_git!(%w(rev-parse HEAD), chdir: rebase_path, env: env).strip
|
||||
|
||||
Gitlab::Git::OperationService.new(user, self)
|
||||
.update_branch(branch, rebase_sha, branch_sha)
|
||||
|
||||
rebase_sha
|
||||
end
|
||||
end
|
||||
|
||||
def rebase_in_progress?(rebase_id)
|
||||
fresh_worktree?(worktree_path(REBASE_WORKTREE_PREFIX, rebase_id))
|
||||
end
|
||||
|
||||
def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
|
||||
squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id)
|
||||
env = git_env_for_user(user).merge(
|
||||
'GIT_AUTHOR_NAME' => author.name,
|
||||
'GIT_AUTHOR_EMAIL' => author.email
|
||||
)
|
||||
diff_range = "#{start_sha}...#{end_sha}"
|
||||
diff_files = run_git!(
|
||||
%W(diff --name-only --diff-filter=a --binary #{diff_range})
|
||||
).chomp
|
||||
|
||||
with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
|
||||
# Apply diff of the `diff_range` to the worktree
|
||||
diff = run_git!(%W(diff --binary #{diff_range}))
|
||||
run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
|
||||
stdin.write(diff)
|
||||
end
|
||||
|
||||
# Commit the `diff_range` diff
|
||||
run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
|
||||
|
||||
# Return the squash sha. May print a warning for ambiguous refs, but
|
||||
# we can ignore that with `--quiet` and just take the SHA, if present.
|
||||
# HEAD here always refers to the current HEAD commit, even if there is
|
||||
# another ref called HEAD.
|
||||
run_git!(
|
||||
%w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
|
||||
).chomp
|
||||
end
|
||||
end
|
||||
|
||||
def squash_in_progress?(squash_id)
|
||||
fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
|
||||
end
|
||||
|
||||
def gitaly_repository
|
||||
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
|
||||
end
|
||||
|
@ -1211,6 +1274,57 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def fresh_worktree?(path)
|
||||
File.exist?(path) && !clean_stuck_worktree(path)
|
||||
end
|
||||
|
||||
def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:)
|
||||
base_args = %w(worktree add --detach)
|
||||
|
||||
# Note that we _don't_ want to test for `.present?` here: If the caller
|
||||
# passes an non nil empty value it means it still wants sparse checkout
|
||||
# but just isn't interested in any file, perhaps because it wants to
|
||||
# checkout files in by a changeset but that changeset only adds files.
|
||||
if sparse_checkout_files
|
||||
# Create worktree without checking out
|
||||
run_git!(base_args + ['--no-checkout', worktree_path], env: env)
|
||||
worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path)
|
||||
|
||||
configure_sparse_checkout(worktree_git_path, sparse_checkout_files)
|
||||
|
||||
# After sparse checkout configuration, checkout `branch` in worktree
|
||||
run_git!(%W(checkout --detach #{branch}), chdir: worktree_path, env: env)
|
||||
else
|
||||
# Create worktree and checkout `branch` in it
|
||||
run_git!(base_args + [worktree_path, branch], env: env)
|
||||
end
|
||||
|
||||
yield
|
||||
ensure
|
||||
FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path)
|
||||
FileUtils.rm_rf(worktree_git_path) if worktree_git_path && File.exist?(worktree_git_path)
|
||||
end
|
||||
|
||||
def clean_stuck_worktree(path)
|
||||
return false unless File.mtime(path) < 15.minutes.ago
|
||||
|
||||
FileUtils.rm_rf(path)
|
||||
true
|
||||
end
|
||||
|
||||
# Adding a worktree means checking out the repository. For large repos,
|
||||
# this can be very expensive, so set up sparse checkout for the worktree
|
||||
# to only check out the files we're interested in.
|
||||
def configure_sparse_checkout(worktree_git_path, files)
|
||||
run_git!(%w(config core.sparseCheckout true))
|
||||
|
||||
return if files.empty?
|
||||
|
||||
worktree_info_path = File.join(worktree_git_path, 'info')
|
||||
FileUtils.mkdir_p(worktree_info_path)
|
||||
File.write(File.join(worktree_info_path, 'sparse-checkout'), files)
|
||||
end
|
||||
|
||||
def rugged_fetch_source_branch(source_repository, source_branch, local_ref)
|
||||
with_repo_branch_commit(source_repository, source_branch) do |commit|
|
||||
if commit
|
||||
|
@ -1222,6 +1336,24 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def worktree_path(prefix, id)
|
||||
id = id.to_s
|
||||
raise ArgumentError, "worktree id can't be empty" unless id.present?
|
||||
raise ArgumentError, "worktree id can't contain slashes " if id.include?("/")
|
||||
|
||||
File.join(path, 'gitlab-worktree', "#{prefix}-#{id}")
|
||||
end
|
||||
|
||||
def git_env_for_user(user)
|
||||
{
|
||||
'GIT_COMMITTER_NAME' => user.name,
|
||||
'GIT_COMMITTER_EMAIL' => user.email,
|
||||
'GL_ID' => Gitlab::GlId.gl_id(user),
|
||||
'GL_PROTOCOL' => Gitlab::Git::Hook::GL_PROTOCOL,
|
||||
'GL_REPOSITORY' => gl_repository
|
||||
}
|
||||
end
|
||||
|
||||
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
|
||||
def branches_filter(filter: nil, sort_by: nil)
|
||||
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
|
||||
|
|
|
@ -261,6 +261,27 @@ describe ProjectsController do
|
|||
expect(response).to redirect_to(namespace_project_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project is forked and has a repository', :request_store do
|
||||
let(:public_project) { create(:project, :public, :repository) }
|
||||
let(:other_user) { create(:user) }
|
||||
|
||||
render_views
|
||||
|
||||
before do
|
||||
# View the project as a user that does not have any rights
|
||||
sign_in(other_user)
|
||||
|
||||
fork_project(public_project)
|
||||
end
|
||||
|
||||
it 'does not increase the number of queries when the project is forked' do
|
||||
expected_query = /#{public_project.fork_network.find_forks_in(other_user.namespace).to_sql}/
|
||||
|
||||
expect { get(:show, namespace_id: public_project.namespace, id: public_project) }
|
||||
.not_to exceed_query_limit(1).for_query(expected_query)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update" do
|
||||
|
|
|
@ -146,6 +146,12 @@ describe('Issue model', () => {
|
|||
expect(issue.isFetching.subscriptions).toBe(false);
|
||||
});
|
||||
|
||||
it('sets loading state', () => {
|
||||
issue.setLoadingState('foo', true);
|
||||
|
||||
expect(issue.isLoading.foo).toBe(true);
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('passes assignee ids when there are assignees', (done) => {
|
||||
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable quote-props*/
|
||||
|
||||
const sidebarMockData = {
|
||||
const RESPONSE_MAP = {
|
||||
'GET': {
|
||||
'/gitlab-org/gitlab-shell/issues/5.json': {
|
||||
id: 45,
|
||||
|
@ -66,6 +66,65 @@ const sidebarMockData = {
|
|||
},
|
||||
labels: [],
|
||||
},
|
||||
'/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar': {
|
||||
assignees: [
|
||||
{
|
||||
name: 'User 0',
|
||||
username: 'user0',
|
||||
id: 22,
|
||||
state: 'active',
|
||||
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
|
||||
web_url: 'http: //localhost:3001/user0',
|
||||
},
|
||||
{
|
||||
name: 'Marguerite Bartell',
|
||||
username: 'tajuana',
|
||||
id: 18,
|
||||
state: 'active',
|
||||
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
|
||||
web_url: 'http: //localhost:3001/tajuana',
|
||||
},
|
||||
{
|
||||
name: 'Laureen Ritchie',
|
||||
username: 'michaele.will',
|
||||
id: 16,
|
||||
state: 'active',
|
||||
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
|
||||
web_url: 'http: //localhost:3001/michaele.will',
|
||||
},
|
||||
],
|
||||
human_time_estimate: null,
|
||||
human_total_time_spent: null,
|
||||
participants: [
|
||||
{
|
||||
name: 'User 0',
|
||||
username: 'user0',
|
||||
id: 22,
|
||||
state: 'active',
|
||||
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
|
||||
web_url: 'http: //localhost:3001/user0',
|
||||
},
|
||||
{
|
||||
name: 'Marguerite Bartell',
|
||||
username: 'tajuana',
|
||||
id: 18,
|
||||
state: 'active',
|
||||
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
|
||||
web_url: 'http: //localhost:3001/tajuana',
|
||||
},
|
||||
{
|
||||
name: 'Laureen Ritchie',
|
||||
username: 'michaele.will',
|
||||
id: 16,
|
||||
state: 'active',
|
||||
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
|
||||
web_url: 'http: //localhost:3001/michaele.will',
|
||||
},
|
||||
],
|
||||
subscribed: true,
|
||||
time_estimate: 0,
|
||||
total_time_spent: 0,
|
||||
},
|
||||
'/autocomplete/projects?project_id=15': [
|
||||
{
|
||||
'id': 0,
|
||||
|
@ -113,9 +172,10 @@ const sidebarMockData = {
|
|||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
const mockData = {
|
||||
responseMap: RESPONSE_MAP,
|
||||
mediator: {
|
||||
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
|
||||
endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar',
|
||||
toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
|
||||
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
|
||||
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
|
||||
|
@ -141,12 +201,14 @@ export default {
|
|||
name: 'Administrator',
|
||||
username: 'root',
|
||||
},
|
||||
|
||||
sidebarMockInterceptor(request, next) {
|
||||
const body = sidebarMockData[request.method.toUpperCase()][request.url];
|
||||
|
||||
next(request.respondWith(JSON.stringify(body), {
|
||||
status: 200,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
mockData.sidebarMockInterceptor = function (request, next) {
|
||||
const body = this.responseMap[request.method.toUpperCase()][request.url];
|
||||
|
||||
next(request.respondWith(JSON.stringify(body), {
|
||||
status: 200,
|
||||
}));
|
||||
}.bind(mockData);
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -33,10 +33,29 @@ describe('Sidebar mediator', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('fetches the data', () => {
|
||||
spyOn(this.mediator.service, 'get').and.callThrough();
|
||||
this.mediator.fetch();
|
||||
expect(this.mediator.service.get).toHaveBeenCalled();
|
||||
it('fetches the data', (done) => {
|
||||
const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'];
|
||||
spyOn(this.mediator, 'processFetchedData').and.callThrough();
|
||||
|
||||
this.mediator.fetch()
|
||||
.then(() => {
|
||||
expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('processes fetched data', () => {
|
||||
const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'];
|
||||
this.mediator.processFetchedData(mockData);
|
||||
|
||||
expect(this.mediator.store.assignees).toEqual(mockData.assignees);
|
||||
expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
|
||||
expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
|
||||
expect(this.mediator.store.participants).toEqual(mockData.participants);
|
||||
expect(this.mediator.store.subscribed).toEqual(mockData.subscribed);
|
||||
expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate);
|
||||
expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
|
||||
});
|
||||
|
||||
it('sets moveToProjectId', () => {
|
||||
|
|
|
@ -120,6 +120,12 @@ describe('Sidebar store', () => {
|
|||
expect(this.store.isFetching.participants).toEqual(false);
|
||||
});
|
||||
|
||||
it('sets loading state', () => {
|
||||
this.store.setLoadingState('assignees', true);
|
||||
|
||||
expect(this.store.isLoading.assignees).toEqual(true);
|
||||
});
|
||||
|
||||
it('set time tracking data', () => {
|
||||
this.store.setTimeTrackingData(Mock.time);
|
||||
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
|
||||
|
|
|
@ -531,7 +531,7 @@ describe Namespace do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#has_forks_of?' do
|
||||
describe '#find_fork_of?' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
|
||||
|
||||
|
@ -550,5 +550,13 @@ describe Namespace do
|
|||
|
||||
expect(other_namespace.find_fork_of(project)).to eq(other_fork)
|
||||
end
|
||||
|
||||
context 'with request store enabled', :request_store do
|
||||
it 'only queries once' do
|
||||
expect(project.fork_network).to receive(:find_forks_in).once.and_call_original
|
||||
|
||||
2.times { namespace.find_fork_of(project) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
|
|||
supports_block_expectations
|
||||
|
||||
match do |block|
|
||||
query_count(&block) > expected_count + threshold
|
||||
@subject_block = block
|
||||
actual_count > expected_count + threshold
|
||||
end
|
||||
|
||||
failure_message_when_negated do |actual|
|
||||
|
@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
|
|||
self
|
||||
end
|
||||
|
||||
def for_query(query)
|
||||
@query = query
|
||||
self
|
||||
end
|
||||
|
||||
def threshold
|
||||
@threshold.to_i
|
||||
end
|
||||
|
@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
|
|||
end
|
||||
|
||||
def actual_count
|
||||
@recorder.count
|
||||
@actual_count ||= if @query
|
||||
recorder.log.select { |recorded| recorded =~ @query }.size
|
||||
else
|
||||
recorder.count
|
||||
end
|
||||
end
|
||||
|
||||
def query_count(&block)
|
||||
@recorder = ActiveRecord::QueryRecorder.new(&block)
|
||||
@recorder.count
|
||||
def recorder
|
||||
@recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
|
||||
end
|
||||
|
||||
def count_queries(queries)
|
||||
|
|
Loading…
Reference in New Issue