Commit 3138fcdc authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into 35616-move-k8-to-cluster-page

parents 445a4109 9f75b7a4
...@@ -20,6 +20,7 @@ class ListIssue { ...@@ -20,6 +20,7 @@ class ListIssue {
this.isFetching = { this.isFetching = {
subscriptions: true, subscriptions: true,
}; };
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
...@@ -86,6 +87,10 @@ class ListIssue { ...@@ -86,6 +87,10 @@ class ListIssue {
this.isFetching[key] = value; this.isFetching[key] = value;
} }
setLoadingState(key, value) {
this.isLoading[key] = value;
}
update (url) { update (url) {
const data = { const data = {
issue: { issue: {
......
...@@ -514,10 +514,11 @@ GitLabDropdown = (function() { ...@@ -514,10 +514,11 @@ GitLabDropdown = (function() {
const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle'); const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update'); const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
const hasMultiSelect = dropdownToggle.hasClass('js-multiselect'); const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
// Makes indeterminate items effective // Makes indeterminate items effective
if (this.fullData && hasFilterBulkUpdate) { if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
this.parseData(this.fullData); this.parseData(this.fullData);
} }
......
...@@ -15,7 +15,7 @@ import Cookies from 'js-cookie'; ...@@ -15,7 +15,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.removeListeners = function () { Sidebar.prototype.removeListeners = function () {
this.sidebar.off('click', '.sidebar-collapsed-icon'); 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('loading.gl.dropdown');
$('.dropdown').off('loaded.gl.dropdown'); $('.dropdown').off('loaded.gl.dropdown');
$(document).off('click', '.js-sidebar-toggle'); $(document).off('click', '.js-sidebar-toggle');
...@@ -25,7 +25,7 @@ import Cookies from 'js-cookie'; ...@@ -25,7 +25,7 @@ import Cookies from 'js-cookie';
const $document = $(document); const $document = $(document);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); 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('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
...@@ -180,7 +180,7 @@ import Cookies from 'js-cookie'; ...@@ -180,7 +180,7 @@ import Cookies from 'js-cookie';
var $block, sidebar; var $block, sidebar;
sidebar = e.data; sidebar = e.data;
e.preventDefault(); e.preventDefault();
$block = $(this).closest('.block'); $block = $(e.target).closest('.block');
return sidebar.sidebarDropdownHidden($block); return sidebar.sidebarDropdownHidden($block);
}; };
......
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;
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'; import Mediator from './sidebar_mediator';
import mountSidebar from './mount_sidebar';
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 domContentLoaded() { function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions); const mediator = new Mediator(sidebarOptions);
mediator.fetch(); mediator.fetch();
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees'); mountSidebar(mediator);
// 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');
} }
document.addEventListener('DOMContentLoaded', domContentLoaded); document.addEventListener('DOMContentLoaded', domContentLoaded);
......
...@@ -5,19 +5,23 @@ import Store from './stores/sidebar_store'; ...@@ -5,19 +5,23 @@ import Store from './stores/sidebar_store';
export default class SidebarMediator { export default class SidebarMediator {
constructor(options) { constructor(options) {
if (!SidebarMediator.singleton) { if (!SidebarMediator.singleton) {
this.store = new Store(options); this.initSingleton(options);
this.service = new Service({
endpoint: options.endpoint,
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
});
SidebarMediator.singleton = this;
} }
return SidebarMediator.singleton; 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() { assignYourself() {
this.store.addAssignee(this.store.currentUser); this.store.addAssignee(this.store.currentUser);
} }
...@@ -35,17 +39,21 @@ export default class SidebarMediator { ...@@ -35,17 +39,21 @@ export default class SidebarMediator {
} }
fetch() { fetch() {
this.service.get() return this.service.get()
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then((data) => {
this.store.setAssigneeData(data); this.processFetchedData(data);
this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);
}) })
.catch(() => new Flash('Error occurred when fetching sidebar 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() { toggleSubscription() {
this.store.setFetchingState('subscriptions', true); this.store.setFetchingState('subscriptions', true);
return this.service.toggleSubscription() return this.service.toggleSubscription()
......
...@@ -15,6 +15,7 @@ export default class SidebarStore { ...@@ -15,6 +15,7 @@ export default class SidebarStore {
participants: true, participants: true,
subscriptions: true, subscriptions: true,
}; };
this.isLoading = {};
this.autocompleteProjects = []; this.autocompleteProjects = [];
this.moveToProjectId = 0; this.moveToProjectId = 0;
this.isLockDialogOpen = false; this.isLockDialogOpen = false;
...@@ -55,6 +56,10 @@ export default class SidebarStore { ...@@ -55,6 +56,10 @@ export default class SidebarStore {
this.isFetching[key] = value; this.isFetching[key] = value;
} }
setLoadingState(key, value) {
this.isLoading[key] = value;
}
addAssignee(assignee) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.assignees.push(assignee); this.assignees.push(assignee);
......
...@@ -139,7 +139,17 @@ class Namespace < ActiveRecord::Base ...@@ -139,7 +139,17 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project) def find_fork_of(project)
return nil unless project.fork_network 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 end
def lfs_enabled? def lfs_enabled?
......
= render 'devise/shared/tab_single', tab_title:'Change your password' = render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box .login-box
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
= f.hidden_field :reset_password_token = f.hidden_field :reset_password_token
...@@ -17,5 +17,5 @@ ...@@ -17,5 +17,5 @@
.clearfix.prepend-top-20 .clearfix.prepend-top-20
%p %p
%span.light Didn't receive a confirmation email? %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' = render 'devise/shared/sign_in_link'
...@@ -11,6 +11,6 @@ ...@@ -11,6 +11,6 @@
= f.check_box :remember_me, class: 'remember-me-checkbox' = f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me %span Remember me
.pull-right.forgot-password .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 .submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save" = f.submit "Sign in", class: "btn btn-save"
<%- if controller_name != 'sessions' %> <%- 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 -%> <% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %> <%- 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 -%> <% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %> <%- 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 -%> <% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> <%- 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 -%> <% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%- 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 -%> <% end -%>
%p %p
%span.light %span.light
Already have login and password? 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 @@ ...@@ -31,4 +31,4 @@
%p %p
%span.light Didn't receive a confirmation email? %span.light Didn't receive a confirmation email?
= succeed '.' do = 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 @@ ...@@ -9,7 +9,7 @@
%p Try logging in using your username or email. If you have forgotten your password, try recovering it %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 "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 %hr
%p.light If none of the options work, try contacting a GitLab administrator. %p.light If none of the options work, try contacting a GitLab administrator.
- type = local_assigns.fetch(:type, :issues) - type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters
%li{ class: active_when(params[:state] == 'opened') }> %li{ class: active_when(params[:state] == 'opened') }>
...@@ -20,6 +19,4 @@ ...@@ -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 = 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)} #{issuables_state_counter_text(type, :closed)}
%li{ class: active_when(params[:state] == 'all') }> = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :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)}
- 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}
---
title: Confirming email with invalid token should no longer generate an error
merge_request: 15726
author:
type: fixed
---
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. ...@@ -152,12 +152,23 @@ CE and EE.
## Previewing the changes live ## Previewing the changes live
If you want to preview the doc changes of your merge request live, you can use 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:** TIP: **Tip:**
If your branch contains only documentation changes, you can use If your branch contains only documentation changes, you can use
[special branch names](#testing) to avoid long running pipelines. [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) ![Manual trigger a docs build](img/manual_build_docs.png)
This job will: This job will:
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module Git module Git
module Conflict module Conflict
class File 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) def initialize(repository, commit_oid, conflict, content)
@repository = repository @repository = repository
......
...@@ -75,7 +75,7 @@ module Gitlab ...@@ -75,7 +75,7 @@ module Gitlab
resolved_lines = file.resolve_lines(params[:sections]) resolved_lines = file.resolve_lines(params[:sections])
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n") 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] elsif params[:content]
new_file = file.resolve_content(params[:content]) new_file = file.resolve_content(params[:content])
end end
......
...@@ -18,6 +18,8 @@ module Gitlab ...@@ -18,6 +18,8 @@ module Gitlab
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze ].freeze
SEARCH_CONTEXT_LINES = 3 SEARCH_CONTEXT_LINES = 3
REBASE_WORKTREE_PREFIX = 'rebase'.freeze
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
NoRepository = Class.new(StandardError) NoRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError) InvalidBlobName = Class.new(StandardError)
...@@ -1070,13 +1072,8 @@ module Gitlab ...@@ -1070,13 +1072,8 @@ module Gitlab
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") 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" input = "update #{ref_path}\x00#{ref}\x00\x00"
output, status = circuit_breaker.perform do run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) }
popen(command, path) { |stdin| stdin.write(input) }
end
raise GitError, output unless status.zero?
end end
def fetch_ref(source_repository, source_ref:, target_ref:) def fetch_ref(source_repository, source_ref:, target_ref:)
...@@ -1098,14 +1095,22 @@ module Gitlab ...@@ -1098,14 +1095,22 @@ module Gitlab
end end
# Refactoring aid; allows us to copy code from app/models/repository.rb # 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 = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice cmd.unshift("nice") if nice
circuit_breaker.perform do circuit_breaker.perform do
popen(cmd, path, env) popen(cmd, chdir, env, &block)
end end
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 # Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git_with_timeout(args, timeout, env: {}) def run_git_with_timeout(args, timeout, env: {})
circuit_breaker.perform do circuit_breaker.perform do
...@@ -1175,6 +1180,64 @@ module Gitlab ...@@ -1175,6 +1180,64 @@ module Gitlab
raise GitError.new("Could not fsck repository:\n#{output}") unless status.zero? raise GitError.new("Could not fsck repository:\n#{output}") unless status.zero?
end 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 def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository) Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end end
...@@ -1211,6 +1274,57 @@ module Gitlab ...@@ -1211,6 +1274,57 @@ module Gitlab
private 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) def rugged_fetch_source_branch(source_repository, source_branch, local_ref)
with_repo_branch_commit(source_repository, source_branch) do |commit| with_repo_branch_commit(source_repository, source_branch) do |commit|
if commit if commit
...@@ -1222,6 +1336,24 @@ module Gitlab ...@@ -1222,6 +1336,24 @@ module Gitlab
end end
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'. # 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) def branches_filter(filter: nil, sort_by: nil)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
......
...@@ -261,6 +261,27 @@ describe ProjectsController do ...@@ -261,6 +261,27 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path) expect(response).to redirect_to(namespace_project_path)
end end
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 end
describe "#update" do describe "#update" do
......
...@@ -146,6 +146,12 @@ describe('Issue model', () => { ...@@ -146,6 +146,12 @@ describe('Issue model', () => {
expect(issue.isFetching.subscriptions).toBe(false); expect(issue.isFetching.subscriptions).toBe(false);
}); });
it('sets loading state', () => {
issue.setLoadingState('foo', true);
expect(issue.isLoading.foo).toBe(true);
});
describe('update', () => { describe('update', () => {
it('passes assignee ids when there are assignees', (done) => { it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => { spyOn(Vue.http, 'patch').and.callFake((url, data) => {
......
/* eslint-disable quote-props*/ /* eslint-disable quote-props*/
const sidebarMockData = { const RESPONSE_MAP = {
'GET': { 'GET': {
'/gitlab-org/gitlab-shell/issues/5.json': { '/gitlab-org/gitlab-shell/issues/5.json': {
id: 45, id: 45,
...@@ -66,6 +66,65 @@ const sidebarMockData = { ...@@ -66,6 +66,65 @@ const sidebarMockData = {
}, },
labels: [], 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': [ '/autocomplete/projects?project_id=15': [
{ {
'id': 0, 'id': 0,
...@@ -113,9 +172,10 @@ const sidebarMockData = { ...@@ -113,9 +172,10 @@ const sidebarMockData = {
}, },
}; };
export default { const mockData = {
responseMap: RESPONSE_MAP,
mediator: { 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', toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
...@@ -141,12 +201,14 @@ export default { ...@@ -141,12 +201,14 @@ export default {
name: 'Administrator', name: 'Administrator',
username: 'root', username: 'root',
}, },
};
sidebarMockInterceptor(request, next) { mockData.sidebarMockInterceptor = function (request, next) {
const body = sidebarMockData[request.method.toUpperCase()][request.url]; const body = this.responseMap[request.method.toUpperCase()][request.url];
next(request.respondWith(JSON.stringify(body), { next(request.respondWith(JSON.stringify(body), {
status: 200, status: 200,
})); }));
}, }.bind(mockData);
};
export default mockData;
...@@ -33,10 +33,29 @@ describe('Sidebar mediator', () => { ...@@ -33,10 +33,29 @@ describe('Sidebar mediator', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('fetches the data', () => { it('fetches the data', (done) => {
spyOn(this.mediator.service, 'get').and.callThrough(); const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'];
this.mediator.fetch(); spyOn(this.mediator, 'processFetchedData').and.callThrough();
expect(this.mediator.service.get).toHaveBeenCalled();
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', () => { it('sets moveToProjectId', () => {
......
...@@ -120,6 +120,12 @@ describe('Sidebar store', () => { ...@@ -120,6 +120,12 @@ describe('Sidebar store', () => {
expect(this.store.isFetching.participants).toEqual(false); 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', () => { it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time); this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate); expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
......
...@@ -531,7 +531,7 @@ describe Namespace do ...@@ -531,7 +531,7 @@ describe Namespace do
end end
end end
describe '#has_forks_of?' do describe '#find_fork_of?' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) } let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
...@@ -550,5 +550,13 @@ describe Namespace do ...@@ -550,5 +550,13 @@ describe Namespace do
expect(other_namespace.find_fork_of(project)).to eq(other_fork) expect(other_namespace.find_fork_of(project)).to eq(other_fork)
end 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
end end
...@@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected| ...@@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations supports_block_expectations
match do |block| match do |block|
query_count(&block) > expected_count + threshold @subject_block = block
actual_count > expected_count + threshold
end end
failure_message_when_negated do |actual| failure_message_when_negated do |actual|
...@@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected| ...@@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
self self
end end
def for_query(query)
@query = query
self
end
def threshold def threshold
@threshold.to_i @threshold.to_i
end end
...@@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected| ...@@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
end end
def actual_count def actual_count
@recorder.count @actual_count ||= if @query
recorder.log.select { |recorded| recorded =~ @query }.size
else
recorder.count
end
end end
def query_count(&block) def recorder
@recorder = ActiveRecord::QueryRecorder.new(&block) @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
@recorder.count
end end
def count_queries(queries) def count_queries(queries)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment