未验证 提交 b70d828f 编写于 作者: L Luke "Jared" Bennett

Merge branch 'master' into new-resolvable-discussion

/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* eslint-disable func-names, wrap-iife, no-use-before-define,
consistent-return, prefer-rest-params */
/* global Breakpoints */
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
var AUTO_SCROLL_OFFSET = 75;
var DOWN_BUILD_TRACE = '#down-build-trace';
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace';
window.Build = (function() {
window.Build = (function () {
Build.timeout = null;
Build.state = null;
function Build(options) {
options = options || $('.js-build-options').data();
this.pageUrl = options.pageUrl;
this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
this.state = options.logState;
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl;
this.buildUrl = this.options.buildUrl;
this.buildStatus = this.options.buildStatus;
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
this.updateDropdown = bind(this.updateDropdown, this);
this.$body = $('body');
this.$buildTrace = $('#build-trace');
this.$autoScrollContainer = $('.autoscroll-container');
......@@ -29,112 +33,110 @@ window.Build = (function() {
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
this.$buildScroll = $('#js-build-scroll');
this.$truncatedInfo = $('.js-truncated-info');
clearTimeout(Build.timeout);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar();
this.$buildScroll = $('#js-build-scroll');
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
$(window)
.off('resize.build')
.on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll)
.off('click.stepTrace')
.on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
this.initScrollButtonAffix();
}
this.initScrollButtonAffix();
this.invokeBuildTrace();
}
Build.prototype.initSidebar = function() {
Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
};
Build.prototype.location = function() {
return window.location.href.split("#")[0];
Build.prototype.invokeBuildTrace = function () {
return this.getBuildTrace();
};
Build.prototype.invokeBuildTrace = function() {
var continueRefreshStatuses = ['running', 'pending'];
// Continue to update build trace when build is running or pending
if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
Build.timeout = setTimeout((function(_this) {
return function() {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
})(this), 4000);
}
};
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
Build.prototype.getBuildTrace = function () {
return $.ajax({
url: this.pageUrl + "/trace.json",
url: `${this.pageUrl}/trace.json`,
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.html);
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
data: {
state: this.state,
},
success: ((log) => {
const $buildContainer = $('.js-build-output');
if (log.state) {
this.state = log.state;
}
if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
if (log.append) {
$buildContainer.append(log.html);
} else {
$buildContainer.html(log.html);
if (log.truncated) {
$('.js-truncated-info-size').html(` ${log.size} `);
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else {
this.$truncatedInfo.addClass('hidden');
}
}
this.checkAutoscroll();
if (!log.complete) {
Build.timeout = setTimeout(() => {
this.invokeBuildTrace();
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
}
}.bind(this)
});
};
Build.prototype.getBuildTrace = function() {
return $.ajax({
url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
var pageUrl;
if (log.state) {
_this.state = log.state;
}
_this.invokeBuildTrace();
if (log.status === "running") {
if (log.append) {
$('.js-build-output').append(log.html);
} else {
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) {
pageUrl = _this.pageUrl;
if (_this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
return gl.utils.visitUrl(pageUrl);
if (log.status !== this.buildStatus) {
let pageUrl = this.pageUrl;
if (this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
};
})(this)
gl.utils.visitUrl(pageUrl);
}
}),
error: () => {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
},
});
};
Build.prototype.checkAutoscroll = function() {
if (this.$autoScrollStatus.data("state") === "enabled") {
return $("html,body").scrollTop(this.$buildTrace.height());
Build.prototype.checkAutoscroll = function () {
if (this.$autoScrollStatus.data('state') === 'enabled') {
return $('html,body').scrollTop(this.$buildTrace.height());
}
// Handle a situation where user started new build
......@@ -146,7 +148,7 @@ window.Build = (function() {
}
};
Build.prototype.initScrollButtonAffix = function() {
Build.prototype.initScrollButtonAffix = function () {
// Hide everything initially
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
......@@ -167,15 +169,17 @@ window.Build = (function() {
// - Show Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
Build.prototype.initScrollMonitor = function() {
if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
Build.prototype.initScrollMonitor = function () {
if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is somewhere in middle of Build Log
this.$scrollTopBtn.show();
if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
this.$scrollBottomBtn.show();
} else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
} else if (this.$buildRefreshAnimation.is(':visible') &&
!gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
} else {
this.$scrollBottomBtn.hide();
......@@ -186,10 +190,13 @@ window.Build = (function() {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else {
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollContainer.css({
top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
}).show();
this.$autoScrollStatusText.addClass('animate');
}
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is at Top of Build Log
this.$scrollTopBtn.hide();
......@@ -197,17 +204,22 @@ window.Build = (function() {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
(this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
} else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
(this.$buildRefreshAnimation.is(':visible') &&
gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
// User is at Bottom of Build Log
this.$scrollTopBtn.show();
this.$scrollBottomBtn.hide();
// Show and Reposition Autoscroll Status Indicator
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollContainer.css({
top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
}).show();
this.$autoScrollStatusText.addClass('animate');
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// Build Log height is small
this.$scrollTopBtn.hide();
......@@ -218,65 +230,81 @@ window.Build = (function() {
this.$autoScrollStatusText.removeClass('animate');
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
if (this.buildStatus === 'running' || this.buildStatus === 'pending') {
// Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
this.$autoScrollStatus.data(
'state',
gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled',
);
}
};
Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
Build.prototype.shouldHideSidebarForViewport = function () {
const bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$truncatedInfo.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
};
Build.prototype.sidebarOnResize = function() {
Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
};
Build.prototype.sidebarOnClick = function() {
Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
var $date, date;
$date = $('.js-artifacts-remove');
Build.prototype.updateArtifactRemoveDate = function () {
const $date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
const date = $date.text();
return $date.text(
gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
);
}
};
Build.prototype.populateJobs = function(stage) {
Build.prototype.populateJobs = function (stage) {
$('.build-job').hide();
$('.build-job[data-stage="' + stage + '"]').show();
$(`.build-job[data-stage="${stage}"]`).show();
};
Build.prototype.updateStageDropdownText = function(stage) {
Build.prototype.updateStageDropdownText = function (stage) {
$('.stage-selection').text(stage);
};
Build.prototype.updateDropdown = function(e) {
Build.prototype.updateDropdown = function (e) {
e.preventDefault();
var stage = e.currentTarget.text;
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
};
Build.prototype.stepTrace = function(e) {
var $currentTarget;
Build.prototype.stepTrace = function (e) {
e.preventDefault();
$currentTarget = $(e.currentTarget);
const $currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
offset: 0
offset: 0,
});
};
Build.prototype.initAffixTruncatedInfo = function () {
const offsetTop = this.$buildTrace.offset().top;
this.$truncatedInfo.affix({
offset: {
top: offsetTop,
},
});
};
......
......@@ -57,6 +57,37 @@
margin-right: 5px;
}
}
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black-transparent;
height: 45px;
&.affix {
top: 0;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
}
&.affix-top {
position: absolute;
top: 0;
margin: 0 auto;
right: 5px;
left: 5px;
}
}
}
.scroll-controls {
......@@ -186,6 +217,7 @@
white-space: pre;
overflow-x: auto;
font-size: 12px;
position: relative;
.fa-refresh {
font-size: 24px;
......
......@@ -196,6 +196,7 @@
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
z-index: 2;
&.right-sidebar-expanded {
width: $gutter_width;
......
module RequiresHealthToken
extend ActiveSupport::Concern
included do
before_action :validate_health_check_access!
end
private
def validate_health_check_access!
render_404 unless token_valid?
end
def token_valid?
token = params[:token].presence || request.headers['TOKEN']
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
)
end
def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404'
end
end
class HealthCheckController < HealthCheck::HealthCheckController
before_action :validate_health_check_access!
private
def validate_health_check_access!
render_404 unless token_valid?
end
def token_valid?
token = params[:token].presence || request.headers['TOKEN']
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
)
end
def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404'
end
include RequiresHealthToken
end
class HealthController < ActionController::Base
protect_from_forgery with: :exception
include RequiresHealthToken
CHECKS = [
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::RedisCheck,
Gitlab::HealthChecks::FsShardsCheck,
].freeze
def readiness
results = CHECKS.map { |check| [check.name, check.readiness] }
render_check_results(results)
end
def liveness
results = CHECKS.map { |check| [check.name, check.liveness] }
render_check_results(results)
end
def metrics
results = CHECKS.flat_map(&:metrics)
response = results.map(&method(:metric_to_prom_line)).join("\n")
render text: response, content_type: 'text/plain; version=0.0.4'
end
private
def metric_to_prom_line(metric)
labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || ''
if labels.empty?
"#{metric.name} #{metric.value}"
else
"#{metric.name}{#{labels}} #{metric.value}"
end
end
def render_check_results(results)
flattened = results.flat_map do |name, result|
if result.is_a?(Gitlab::HealthChecks::Result)
[[name, result]]
else
result.map { |r| [name, r] }
end
end
success = flattened.all? { |name, r| r.success }
response = flattened.map do |name, r|
info = { status: r.success ? 'ok' : 'failed' }
info['message'] = r.message if r.message
info[:labels] = r.labels if r.labels
[name, info]
end
render json: response.to_h, status: success ? :ok : :service_unavailable
end
end
......@@ -153,10 +153,6 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero?
end
def is_empty?(user = nil)
total_items_count(user).zero?
end
def author_id
nil
end
......
......@@ -91,7 +91,7 @@ class JiraService < IssueTrackerService
{ type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '2' }
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
]
end
......
......@@ -1156,6 +1156,8 @@ class Repository
@project.repository_storage_path
end
delegate :gitaly_channel, :gitaly_repository, to: :raw_repository
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end
......
......@@ -10,6 +10,7 @@ class GlobalPolicy < BasePolicy
can! :access_api
can! :access_git
can! :receive_notifications
can! :use_slash_commands
end
end
end
......@@ -7,6 +7,8 @@ module SlashCommands
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
def execute(content, issuable)
return [content, {}] unless current_user.can?(:use_slash_commands)
@issuable = issuable
@updates = {}
......
......@@ -47,17 +47,19 @@
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw')
%span.badge.issues-count
= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
- issues_count = cached_assigned_issuables_count(current_user, :issues, :opened)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
%li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
%span.badge.merge-requests-count
= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
- merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
%li
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('check-circle fw')
%span.badge.todos-count
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
......
......@@ -71,6 +71,11 @@
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
.js-truncated-info.truncated-info.hidden
%span<
Showing last
%span.js-truncated-info-size><
KiB of log
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
......
......@@ -8,9 +8,9 @@
%h3.page-title= @environment.name
.col-md-5
.nav-controls
= render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
......
---
title: Add /-/readiness /-/liveness and /-/metrics endpoints to track application health
merge_request: 10416
author:
---
title: Shows 'Go Back' link only when browser history is available
merge_request: 9017
author:
---
title: Remove confusing placeholder for JIRA transition_id
merge_request:
author:
---
title: Removed Milestone#is_empty?
merge_request: 10523
author: Jacopo Beschi @jacopo-beschi
---
title: fix Status icons overlapping sidebar on mobile
merge_request:
author:
---
title: Hide header counters for issue/mr/todos if zero
merge_request: 10506
author:
---
title: Moved the monitoring button inside the show view for the environments page
merge_request:
author:
......@@ -39,6 +39,12 @@ Rails.application.routes.draw do
# Health check
get 'health_check(/:checks)' => 'health_check#index', as: :health_check
scope path: '-', controller: 'health' do
get :liveness
get :readiness
get :metrics
end
# Koding route
get 'koding' => 'koding#index'
......
......@@ -18,62 +18,62 @@ All technical content published by GitLab lives in the documentation, including:
- [Account Security](user/profile/account/two_factor_authentication.md) Securing your account via two-factor authentication, etc.
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages.
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
- [Snippets](user/snippets.md) Snippets allow you to create little bits of code.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
## Administrator documentation
- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols) Define which Git access protocols can be used to talk to GitLab
- [Authentication/Authorization](administration/auth/README.md) Configure
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Authentication/Authorization](administration/auth/README.md) Configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
- [Custom Git hooks](administration/custom_hooks.md) Custom Git hooks (on the filesystem) for when webhooks aren't enough.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header.
- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
- [Install](install/README.md) Requirements, directory structures and installation from source.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab.
- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Operations](administration/operations.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
- [Repository storage paths](administration/repository_storage_paths.md) Manage the paths used to store repositories.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header.
- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
## Contributor documentation
......
......@@ -101,7 +101,7 @@ in the table below.
| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
After saving the configuration, your GitLab project will be able to interact
with the linked JIRA project.
......
# Search through GitLab
## Issues and merge requests
To search through issues and merge requests in multiple projects, you can use the left-sidebar.
Click the menu bar, then **Issues** or **Merge Requests**, which work in the same way,
therefore, the following notes are valid for both.
The number displayed on their right represents the number of issues and merge requests assigned to you.
![menu bar - issues and MRs assigned to you](img/left_menu_bar.png)
When you click **Issues**, you'll see the opened issues assigned to you straight away:
![Issues assigned to you](img/issues_assigned_to_you.png)
You can filter them by **Author**, **Assignee**, **Milestone**, and **Labels**,
searching through **Open**, **Closed**, and **All** issues.
Of course, you can combine all filters together.
### Issues and MRs assigned to you or created by you
You'll find a shortcut to issues and merge requests create by you or assigned to you
on the search field on the top-right of your screen:
![shortcut to your issues and mrs](img/issues_mrs_shortcut.png)
## Issues and merge requests per project
If you want to search for issues present in a specific project, navigate to
a project's **Issues** tab, and click on the field **Search or filter results...**. It will
display a dropdown menu, from which you can add filters per author, assignee, milestone, label,
and weight. When done, press **Enter** on your keyboard to filter the issues.
![filter issues in a project](img/filter_issues_project.gif)
The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab,
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
### Shortcut
You'll also find a shortcut on the search field on the top-right of the project's dashboard to
quickly access issues and merge requests created or assigned to you within that project:
![search per project - shortcut](img/project_search.png)
## Todos
Your [todos](../../workflow/todos.md#gitlab-todos) can be searched by "to do" and "done".
You can [filter](../../workflow/todos.md#filtering-your-todos) them per project,
author, type, and action. Also, you can sort them by
[**Label priority**](../../user/project/labels.md#prioritize-labels),
**Last created** and **Oldest created**.
## Projects
You can search through your projects from the left menu, by clicking the menu bar, then **Projects**.
On the field **Filter by name**, type the project or group name you want to find, and GitLab
will filter them for you as you type.
You can also look for the projects you starred (**Starred projects**), and **Explore** all
public and internal projects available in GitLab.com, from which you can filter by visibitily,
through **Trending**, best rated with **Most starts**, or **All** of them.
You can also sort them by **Name**, **Last created**, **Oldest created**, **Last updated**,
**Oldest updated**, **Owner**, and choose to hide or show **archived projects**:
![sort projects](img/sort_projects.png)
## Groups
Similarly to [projects search](#projects), you can search through your groups from
the left menu, by clicking the menu bar, then **Groups**.
On the field **Filter by name**, type the group name you want to find, and GitLab
will filter them for you as you type.
You can also **Explore** all public and internal groups available in GitLab.com,
and sort them by **Last created**, **Oldest created**, **Last updated**, or **Oldest updated**.
## Issue Boards
From an [Issue Board](../../user/project/issue_board.md), you can filter issues by **Author**, **Assignee**, **Milestone**, and **Labels**.
You can also filter them by name (issue title), from the field **Filter by name**, which is loaded as you type.
When you want to search for issues to add to lists present in your Issue Board, click
the button **Add issues** on the top-right of your screen, opening a modal window from which
you'll be able to, besides filtering them by **Name**, **Author**, **Assignee**, **Milestone**,
and **Labels**, select multiple issues to add to a list of your choice:
![search and select issues to add to board](img/search_issues_board.png)
......@@ -142,7 +142,7 @@ module API
project = Project.find_by_full_path(relative_path.sub(/\.(git|wiki)\z/, ''))
begin
Gitlab::GitalyClient::Notifications.new(project.repository_storage, relative_path).post_receive
Gitlab::GitalyClient::Notifications.new(project.repository).post_receive
rescue GRPC::Unavailable => e
render_api_error(e, 500)
end
......
......@@ -5,7 +5,11 @@ require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler].freeze
HANDLERS = [
UnsubscribeHandler,
CreateNoteHandler,
CreateIssueHandler
].freeze
def self.for(mail, mail_key)
HANDLERS.find do |klass|
......
......@@ -968,6 +968,14 @@ module Gitlab
@attributes.attributes(path)
end
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@repository_storage, @relative_path)
end
def gitaly_channel
Gitlab::GitalyClient.get_channel(@repository_storage)
end
private
# Get the content of a blob for a given commit. If the blob is a commit
......@@ -1247,7 +1255,7 @@ module Gitlab
end
def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(@repository_storage, @relative_path)
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end
end
end
......
......@@ -7,14 +7,13 @@ module Gitlab
class << self
def diff_from_parent(commit, options = {})
project = commit.project
channel = GitalyClient.get_channel(project.repository_storage)
stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: channel)
repo = Gitaly::Repository.new(path: project.repository.path_to_repo)
parent = commit.parents[0]
repository = commit.project.repository
gitaly_repo = repository.gitaly_repository
stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
parent = commit.parents[0]
parent_id = parent ? parent.id : EMPTY_TREE_ID
request = Gitaly::CommitDiffRequest.new(
repository: repo,
request = Gitaly::CommitDiffRequest.new(
repository: gitaly_repo,
left_commit_id: parent_id,
right_commit_id: commit.id
)
......@@ -23,12 +22,10 @@ module Gitlab
end
def is_ancestor(repository, ancestor_id, child_id)
project = Project.find_by_path(repository.path)
channel = GitalyClient.get_channel(project.repository_storage)
stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: channel)
repo = Gitaly::Repository.new(path: repository.path_to_repo)
gitaly_repo = repository.gitaly_repository
stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
request = Gitaly::CommitIsAncestorRequest.new(
repository: repo,
repository: gitaly_repo,
ancestor_id: ancestor_id,
child_id: child_id
)
......
......@@ -3,13 +3,14 @@ module Gitlab
class Notifications
attr_accessor :stub
def initialize(repository_storage, relative_path)
@channel, @repository = Util.process_path(repository_storage, relative_path)
@stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: @channel)
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
end
def post_receive
request = Gitaly::PostReceiveRequest.new(repository: @repository)
request = Gitaly::PostReceiveRequest.new(repository: @gitaly_repo)
@stub.post_receive(request)
end
end
......
......@@ -3,23 +3,24 @@ module Gitlab
class Ref
attr_accessor :stub
def initialize(repository_storage, relative_path)
@channel, @repository = Util.process_path(repository_storage, relative_path)
@stub = Gitaly::Ref::Stub.new(nil, nil, channel_override: @channel)
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@stub = Gitaly::Ref::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
end
def default_branch_name
request = Gitaly::FindDefaultBranchNameRequest.new(repository: @repository)
request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo)
stub.find_default_branch_name(request).name.gsub(/^refs\/heads\//, '')
end
def branch_names
request = Gitaly::FindAllBranchNamesRequest.new(repository: @repository)
request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
consume_refs_response(stub.find_all_branch_names(request), prefix: 'refs/heads/')
end
def tag_names
request = Gitaly::FindAllTagNamesRequest.new(repository: @repository)
request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo)
consume_refs_response(stub.find_all_tag_names(request), prefix: 'refs/tags/')
end
......
module Gitlab
module GitalyClient
module Util
def self.process_path(repository_storage, relative_path)
channel = GitalyClient.get_channel(repository_storage)
storage_path = Gitlab.config.repositories.storages[repository_storage]['path']
repository = Gitaly::Repository.new(path: File.join(storage_path, relative_path))
[channel, repository]
class << self
def repository(repository_storage, relative_path)
Gitaly::Repository.new(
path: File.join(Gitlab.config.repositories.storages[repository_storage]['path'], relative_path),
storage_name: repository_storage,
relative_path: relative_path,
)
end
end
end
end
......
module Gitlab
module HealthChecks
module BaseAbstractCheck
def name
super.demodulize.underscore
end
def human_name
name.sub(/_check$/, '').capitalize
end
def readiness
raise NotImplementedError
end
def liveness
HealthChecks::Result.new(true)
end
def metrics
[]
end
protected
def metric(name, value, **labels)
Metric.new(name, value, labels)
end
def with_timing(proc)
start = Time.now
result = proc.call
yield result, Time.now.to_f - start.to_f
end
def catch_timeout(seconds, &block)
begin
Timeout.timeout(seconds.to_i, &block)
rescue Timeout::Error => ex
ex
end
end
end
end
end
module Gitlab
module HealthChecks
class DbCheck
extend SimpleAbstractCheck
class << self
private
def metric_prefix
'db_ping'
end
def is_successful?(result)
result == '1'
end
def check
catch_timeout 10.seconds do
if Gitlab::Database.postgresql?
ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.[]('ping')
else
ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.first&.to_s
end
end
end
end
end
end
end
module Gitlab
module HealthChecks
class FsShardsCheck
extend BaseAbstractCheck
class << self
def readiness
repository_storages.map do |storage_name|
begin
tmp_file_path = tmp_file_path(storage_name)
if !storage_stat_test(storage_name)
HealthChecks::Result.new(false, 'cannot stat storage', shard: storage_name)
elsif !storage_write_test(tmp_file_path)
HealthChecks::Result.new(false, 'cannot write to storage', shard: storage_name)
elsif !storage_read_test(tmp_file_path)
HealthChecks::Result.new(false, 'cannot read from storage', shard: storage_name)
else
HealthChecks::Result.new(true, nil, shard: storage_name)
end
rescue RuntimeError => ex
message = "unexpected error #{ex} when checking storage #{storage_name}"
Rails.logger.error(message)
HealthChecks::Result.new(false, message, shard: storage_name)
ensure
delete_test_file(tmp_file_path)
end
end
end
def metrics
repository_storages.flat_map do |storage_name|
tmp_file_path = tmp_file_path(storage_name)
[
operation_metrics(:filesystem_accessible, :filesystem_access_latency, -> { storage_stat_test(storage_name) }, shard: storage_name),
operation_metrics(:filesystem_writable, :filesystem_write_latency, -> { storage_write_test(tmp_file_path) }, shard: storage_name),
operation_metrics(:filesystem_readable, :filesystem_read_latency, -> { storage_read_test(tmp_file_path) }, shard: storage_name)
].flatten
end
end
private
RANDOM_STRING = SecureRandom.hex(1000).freeze
def operation_metrics(ok_metric, latency_metric, operation, **labels)
with_timing operation do |result, elapsed|
[
metric(latency_metric, elapsed, **labels),
metric(ok_metric, result ? 1 : 0, **labels)
]
end
rescue RuntimeError => ex
Rails.logger("unexpected error #{ex} when checking #{ok_metric}")
[metric(ok_metric, 0, **labels)]
end
def repository_storages
@repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages
end
def storages_paths
@storage_paths ||= Gitlab.config.repositories.storages
end
def with_timeout(args)
%w{timeout 1}.concat(args)
end
def tmp_file_path(storage_name)
Dir::Tmpname.create(%w(fs_shards_check +deleted), path(storage_name)) { |path| path }
end
def path(storage_name)
storages_paths&.dig(storage_name, 'path')
end
def storage_stat_test(storage_name)
stat_path = File.join(path(storage_name), '.')
begin
_, status = Gitlab::Popen.popen(with_timeout(%W{ stat #{stat_path} }))
status == 0
rescue Errno::ENOENT
File.exist?(stat_path) && File::Stat.new(stat_path).readable?
end
end
def storage_write_test(tmp_path)
_, status = Gitlab::Popen.popen(with_timeout(%W{ tee #{tmp_path} })) do |stdin|
stdin.write(RANDOM_STRING)
end
status == 0
rescue Errno::ENOENT
written_bytes = File.write(tmp_path, RANDOM_STRING) rescue Errno::ENOENT
written_bytes == RANDOM_STRING.length
end
def storage_read_test(tmp_path)
_, status = Gitlab::Popen.popen(with_timeout(%W{ diff #{tmp_path} - })) do |stdin|
stdin.write(RANDOM_STRING)
end
status == 0
rescue Errno::ENOENT
file_contents = File.read(tmp_path) rescue Errno::ENOENT
file_contents == RANDOM_STRING
end
def delete_test_file(tmp_path)
_, status = Gitlab::Popen.popen(with_timeout(%W{ rm -f #{tmp_path} }))
status == 0
rescue Errno::ENOENT
File.delete(tmp_path) rescue Errno::ENOENT
end
end
end
end
end
module Gitlab::HealthChecks
Metric = Struct.new(:name, :value, :labels)
end
module Gitlab
module HealthChecks
class RedisCheck
extend SimpleAbstractCheck
class << self
private
def metric_prefix
'redis_ping'
end
def is_successful?(result)
result == 'PONG'
end
def check
catch_timeout 10.seconds do
Gitlab::Redis.with(&:ping)
end
end
end
end
end
end
module Gitlab::HealthChecks
Result = Struct.new(:success, :message, :labels)
end
module Gitlab
module HealthChecks
module SimpleAbstractCheck
include BaseAbstractCheck
def readiness
check_result = check
if is_successful?(check_result)
HealthChecks::Result.new(true)
elsif check_result.is_a?(Timeout::Error)
HealthChecks::Result.new(false, "#{human_name} check timed out")
else
HealthChecks::Result.new(false, "unexpected #{human_name} check result: #{check_result}")
end
end
def metrics
with_timing method(:check) do |result, elapsed|
Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless is_successful?(result)
[
metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0),
metric("#{metric_prefix}_success", is_successful?(result) ? 1 : 0),
metric("#{metric_prefix}_latency", elapsed)
]
end
end
private
def metric_prefix
raise NotImplementedError
end
def is_successful?(result)
raise NotImplementedError
end
def check
raise NotImplementedError
end
end
end
end
......@@ -24,14 +24,8 @@ module Gitlab
}
if Gitlab.config.gitaly.enabled
storage = repository.project.repository_storage
address = Gitlab::GitalyClient.get_address(storage)
# TODO: use GitalyClient code to assemble the Repository message
params[:Repository] = Gitaly::Repository.new(
path: repo_path,
storage_name: storage,
relative_path: Gitlab::RepoPath.strip_storage_path(repo_path),
).to_h
address = Gitlab::GitalyClient.get_address(repository.project.repository_storage)
params[:Repository] = repository.gitaly_repository.to_h
feature_enabled = case action.to_s
when 'git_receive_pack'
......
......@@ -57,6 +57,11 @@
.container {
margin: auto 20px;
}
.go-back {
display: none;
}
</style>
</head>
......@@ -71,7 +76,16 @@
<hr />
<p>Make sure the address is correct and that the page hasn't moved.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
<a href="javascript:history.back()">Go back</a>
<a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
</div>
<script>
(function () {
var goBack = document.querySelector('.js-go-back');
if (history.length > 1) {
goBack.style.display = 'inline';
}
})();
</script>
</body>
</html>
......@@ -57,6 +57,11 @@
.container {
margin: auto 20px;
}
.go-back {
display: none;
}
</style>
</head>
......@@ -71,7 +76,17 @@
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
<a href="javascript:history.back()">Go back</a>
<a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
</div>
<script>
(function () {
var goBack = document.querySelector('.js-go-back');
if (history.length > 1) {
goBack.style.display = 'inline';
}
})();
</script>
</body>
</html>
......@@ -57,6 +57,11 @@
.container {
margin: auto 20px;
}
.go-back {
display: none;
}
</style>
</head>
......@@ -71,7 +76,16 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
<a href="javascript:history.back()">Go back</a>
<a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
</div>
<script>
(function () {
var goBack = document.querySelector('.js-go-back');
if (history.length > 1) {
goBack.style.display = 'inline';
}
})();
</script>
</body>
</html>
......@@ -57,6 +57,11 @@
.container {
margin: auto 20px;
}
.go-back {
display: none;
}
</style>
</head>
......@@ -71,7 +76,16 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
<a href="javascript:history.back()">Go back</a>
<a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
</div>
<script>
(function () {
var goBack = document.querySelector('.js-go-back');
if (history.length > 1) {
goBack.style.display = 'inline';
}
})();
</script>
</body>
</html>
......@@ -57,6 +57,11 @@
.container {
margin: auto 20px;
}
.go-back {
display: none;
}
</style>
</head>
......@@ -71,7 +76,16 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
<a href="javascript:history.back()">Go back</a>
<a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
</div>
<script>
(function () {
var goBack = document.querySelector('.js-go-back');
if (history.length > 1) {
goBack.style.display = 'inline';
}
})();
</script>
</body>
</html>
......@@ -5,7 +5,7 @@ module QA
def prepare_test_namespace
return if page.has_content?(Runtime::Namespace.name)
click_on 'New Group'
click_on 'New group'
fill_in 'group_path', with: Runtime::Namespace.name
fill_in 'group_description',
......
require 'spec_helper'
describe HealthController do
include StubENV
let(:token) { current_application_settings.health_check_access_token }
let(:json_response) { JSON.parse(response.body) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
describe '#readiness' do
context 'authorization token provided' do
before do
request.headers['TOKEN'] = token
end
it 'returns proper response' do
get :readiness
expect(json_response['db_check']['status']).to eq('ok')
expect(json_response['redis_check']['status']).to eq('ok')
expect(json_response['fs_shards_check']['status']).to eq('ok')
expect(json_response['fs_shards_check']['labels']['shard']).to eq('default')
end
end
context 'without authorization token' do
it 'returns proper response' do
get :readiness
expect(response.status).to eq(404)
end
end
end
describe '#liveness' do
context 'authorization token provided' do
before do
request.headers['TOKEN'] = token
end
it 'returns proper response' do
get :liveness
expect(json_response['db_check']['status']).to eq('ok')
expect(json_response['redis_check']['status']).to eq('ok')
expect(json_response['fs_shards_check']['status']).to eq('ok')
end
end
context 'without authorization token' do
it 'returns proper response' do
get :liveness
expect(response.status).to eq(404)
end
end
end
describe '#metrics' do
context 'authorization token provided' do
before do
request.headers['TOKEN'] = token
end
it 'returns DB ping metrics' do
get :metrics
expect(response.body).to match(/^db_ping_timeout 0$/)
expect(response.body).to match(/^db_ping_success 1$/)
expect(response.body).to match(/^db_ping_latency [0-9\.]+$/)
end
it 'returns Redis ping metrics' do
get :metrics
expect(response.body).to match(/^redis_ping_timeout 0$/)
expect(response.body).to match(/^redis_ping_success 1$/)
expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/)
end
it 'returns file system check metrics' do
get :metrics
expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/)
expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/)
expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/)
expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/)
expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/)
expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/)
end
end
context 'without authorization token' do
it 'returns proper response' do
get :metrics
expect(response.status).to eq(404)
end
end
end
end
require 'spec_helper'
describe 'Navigation bar counter', feature: true, js: true, caching: true do
describe 'Navigation bar counter', feature: true, caching: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:issue) { create(:issue, project: project) }
......@@ -13,33 +13,48 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
end
it 'reflects dashboard issues count' do
visit issues_dashboard_path
visit issues_path
expect_counters('issues', '1')
issue.update(assignee: nil)
visit issues_dashboard_path
expect_counters('issues', '1')
Timecop.travel(3.minutes.from_now) do
visit issues_path
expect_counters('issues', '0')
end
end
it 'reflects dashboard merge requests count' do
visit merge_requests_dashboard_path
visit merge_requests_path
expect_counters('merge_requests', '1')
merge_request.update(assignee: nil)
visit merge_requests_dashboard_path
expect_counters('merge_requests', '1')
Timecop.travel(3.minutes.from_now) do
visit merge_requests_path
expect_counters('merge_requests', '0')
end
end
def issues_path
issues_dashboard_path(assignee_id: user.id)
end
def merge_requests_path
merge_requests_dashboard_path(assignee_id: user.id)
end
def expect_counters(issuable_type, count)
dashboard_count = find('li.active')
find('.global-dropdown-toggle').click
dashboard_count = find('.nav-links li.active')
nav_count = find(".dashboard-shortcuts-#{issuable_type}")
header_count = find(".header-content .#{issuable_type.tr('_', '-')}-count")
expect(nav_count).to have_content(count)
expect(dashboard_count).to have_content(count)
expect(nav_count).to have_content(count)
expect(header_count).to have_content(count)
end
end
......@@ -64,58 +64,33 @@ describe('Build', () => {
});
});
describe('initial build trace', () => {
beforeEach(() => {
new Build();
});
it('displays the initial build trace', () => {
expect($.ajax.calls.count()).toBe(1);
const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
expect(url).toBe(
`${BUILD_URL}/trace.json`,
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { html: '<span>Example</span>', status: 'running' });
expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
});
it('removes the spinner', () => {
const [{ success, context }] = $.ajax.calls.argsFor(0);
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0);
});
});
describe('running build', () => {
beforeEach(function () {
$('.js-build-options').data('buildStatus', 'running');
this.build = new Build();
spyOn(this.build, 'location').and.returnValue(BUILD_URL);
});
it('updates the build trace on an interval', function () {
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(2);
let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
expect(url).toBe(
`${BUILD_URL}/trace.json?state=`,
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
success.call(context, {
expect($.ajax.calls.count()).toBe(1);
// We have to do it this way to prevent Webpack to fail to compile
// when destructuring assignments and reusing
// the same variables names inside the same scope
let args = $.ajax.calls.argsFor(0)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.success).toEqual(jasmine.any(Function));
args.success.call($, {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
......@@ -123,17 +98,20 @@ describe('Build', () => {
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(3);
[{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
expect($.ajax.calls.count()).toBe(2);
args = $.ajax.calls.argsFor(1)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.data.state).toBe('newstate');
expect(args.success).toEqual(jasmine.any(Function));
success.call(context, {
args.success.call($, {
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
complete: true,
});
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
......@@ -141,19 +119,22 @@ describe('Build', () => {
});
it('replaces the entire build trace', () => {
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
let [{ success, context }] = $.ajax.calls.argsFor(1);
success.call(context, {
let args = $.ajax.calls.argsFor(0)[0];
args.success.call($, {
html: '<span>Update</span>',
status: 'running',
append: true,
append: false,
complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001);
[{ success, context }] = $.ajax.calls.argsFor(2);
success.call(context, {
args = $.ajax.calls.argsFor(1)[0];
args.success.call($, {
html: '<span>Different</span>',
status: 'running',
append: false,
......@@ -163,15 +144,34 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
it('shows information about truncated log', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
truncated: true,
size: '50',
});
expect(
$('#build-trace .js-truncated-info').text().trim(),
).toContain('Showing last 50 KiB of log');
expect($('#build-trace .js-truncated-info-size').text()).toMatch('50');
});
it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1);
success.call(context, {
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Final</span>',
status: 'passed',
append: true,
complete: true,
});
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
......
......@@ -4,7 +4,7 @@ describe Gitlab::GitalyClient::Commit do
describe '.diff_from_parent' do
let(:diff_stub) { double('Gitaly::Diff::Stub') }
let(:project) { create(:project, :repository) }
let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) }
let(:repository_message) { project.repository.gitaly_repository }
let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
before do
......
......@@ -4,7 +4,7 @@ describe Gitlab::GitalyClient::Notifications do
describe '#post_receive' do
let(:project) { create(:empty_project) }
let(:repo_path) { project.repository.path_to_repo }
subject { described_class.new(project.repository_storage, project.full_path + '.git') }
subject { described_class.new(project.repository) }
it 'sends a post_receive message' do
expect_any_instance_of(Gitaly::Notifications::Stub).
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::GitalyClient::Ref do
let(:project) { create(:empty_project) }
let(:repo_path) { project.repository.path_to_repo }
let(:client) { Gitlab::GitalyClient::Ref.new(project.repository_storage, project.full_path + '.git') }
let(:client) { Gitlab::GitalyClient::Ref.new(project.repository) }
before do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
......
require 'spec_helper'
require_relative './simple_check_shared'
describe Gitlab::HealthChecks::DbCheck do
include_examples 'simple_check', 'db_ping', 'Db', '1'
end
require 'spec_helper'
describe Gitlab::HealthChecks::FsShardsCheck do
let(:metric_class) { Gitlab::HealthChecks::Metric }
let(:result_class) { Gitlab::HealthChecks::Result }
let(:repository_storages) { [:default] }
let(:tmp_dir) { Dir.mktmpdir }
let(:storages_paths) do
{
default: { path: tmp_dir }
}.with_indifferent_access
end
before do
allow(described_class).to receive(:repository_storages) { repository_storages }
allow(described_class).to receive(:storages_paths) { storages_paths }
end
after do
FileUtils.remove_entry_secure(tmp_dir) if Dir.exist?(tmp_dir)
end
shared_examples 'filesystem checks' do
describe '#readiness' do
subject { described_class.readiness }
context 'storage points to not existing folder' do
let(:storages_paths) do
{
default: { path: 'tmp/this/path/doesnt/exist' }
}.with_indifferent_access
end
it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
end
context 'storage points to directory that has both read and write rights' do
before do
FileUtils.chmod_R(0755, tmp_dir)
end
it { is_expected.to include(result_class.new(true, nil, shard: :default)) }
it 'cleans up files used for testing' do
expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original
subject
expect(Dir.entries(tmp_dir).count).to eq(2)
end
context 'read test fails' do
before do
allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false)
end
it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: :default)) }
end
context 'write test fails' do
before do
allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false)
end
it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: :default)) }
end
end
end
describe '#metrics' do
subject { described_class.metrics }
context 'storage points to not existing folder' do
let(:storages_paths) do
{
default: { path: 'tmp/this/path/doesnt/exist' }
}.with_indifferent_access
end
it { is_expected.to include(metric_class.new(:filesystem_accessible, 0, shard: :default)) }
it { is_expected.to include(metric_class.new(:filesystem_readable, 0, shard: :default)) }
it { is_expected.to include(metric_class.new(:filesystem_writable, 0, shard: :default)) }
it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be > 0, labels: { shard: :default })) }
it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be > 0, labels: { shard: :default })) }
it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be > 0, labels: { shard: :default })) }
end
context 'storage points to directory that has both read and write rights' do
before do
FileUtils.chmod_R(0755, tmp_dir)
end
it { is_expected.to include(metric_class.new(:filesystem_accessible, 1, shard: :default)) }
it { is_expected.to include(metric_class.new(:filesystem_readable, 1, shard: :default)) }
it { is_expected.to include(metric_class.new(:filesystem_writable, 1, shard: :default)) }
it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be > 0, labels: { shard: :default })) }
it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be > 0, labels: { shard: :default })) }
it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be > 0, labels: { shard: :default })) }
end
end
end
context 'when popen always finds required binaries' do
before do
allow(Gitlab::Popen).to receive(:popen).and_wrap_original do |method, *args, &block|
begin
method.call(*args, &block)
rescue RuntimeError
raise 'expected not to happen'
end
end
end
it_behaves_like 'filesystem checks'
end
context 'when popen never finds required binaries' do
before do
allow(Gitlab::Popen).to receive(:popen).and_raise(Errno::ENOENT)
end
it_behaves_like 'filesystem checks'
end
end
require 'spec_helper'
require_relative './simple_check_shared'
describe Gitlab::HealthChecks::RedisCheck do
include_examples 'simple_check', 'redis_ping', 'Redis', 'PONG'
end
shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
describe '#metrics' do
subject { described_class.metrics }
context 'Check is passing' do
before do
allow(described_class).to receive(:check).and_return success_result
end
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 1)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 0)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be > 0)) }
end
context 'Check is misbehaving' do
before do
allow(described_class).to receive(:check).and_return 'error!'
end
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 0)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 0)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be > 0)) }
end
context 'Check is timeouting' do
before do
allow(described_class).to receive(:check).and_return Timeout::Error.new
end
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_success", value: 0)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_timeout", value: 1)) }
it { is_expected.to include(have_attributes(name: "#{metrics_prefix}_latency", value: be > 0)) }
end
end
describe '#readiness' do
subject { described_class.readiness }
context 'Check returns ok' do
before do
allow(described_class).to receive(:check).and_return success_result
end
it { is_expected.to have_attributes(success: true) }
end
context 'Check is misbehaving' do
before do
allow(described_class).to receive(:check).and_return 'error!'
end
it { is_expected.to have_attributes(success: false, message: "unexpected #{check_name} check result: error!") }
end
context 'Check is timeouting' do
before do
allow(described_class).to receive(:check ).and_return Timeout::Error.new
end
it { is_expected.to have_attributes(success: false, message: "#{check_name} check timed out") }
end
end
describe '#liveness' do
subject { described_class.readiness }
it { is_expected.to eq(Gitlab::HealthChecks::Result.new(true)) }
end
end
......@@ -109,18 +109,6 @@ describe Milestone, models: true do
it { expect(milestone.percent_complete(user)).to eq(75) }
end
describe '#is_empty?' do
before do
milestone.issues << create(:issue, project: project)
milestone.issues << create(:closed_issue, project: project)
milestone.merge_requests << create(:merge_request)
end
it { expect(milestone.closed_items_count(user)).to eq(1) }
it { expect(milestone.total_items_count(user)).to eq(3) }
it { expect(milestone.is_empty?(user)).to be_falsey }
end
describe '#can_be_closed?' do
it { expect(milestone.can_be_closed?).to be_truthy }
end
......
......@@ -895,8 +895,8 @@ bootstrap-sass@^3.3.6:
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz#6596c7ab40f6637393323ab0bc80d064fc630498"
brace-expansion@^1.0.0:
version "1.1.6"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9"
version "1.1.7"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59"
dependencies:
balanced-match "^0.4.1"
concat-map "0.0.1"
......@@ -970,7 +970,7 @@ browserify-zlib@^0.1.4:
dependencies:
pako "~0.2.0"
buffer-shims@^1.0.0:
buffer-shims@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
......@@ -2416,8 +2416,8 @@ ieee754@^1.1.4:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
ignore@^3.2.0:
version "3.2.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.6.tgz#26e8da0644be0bb4cb39516f6c79f0e0f4ffe48c"
version "3.2.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd"
immediate@~3.0.5:
version "3.0.6"
......@@ -3773,19 +3773,19 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.6.tgz#8b43aed76e71483938d12a8d46c6cf1a00b1f816"
"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
version "2.2.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.8.tgz#ad28b686f3554c73d39bc32347fa058356624705"
dependencies:
buffer-shims "^1.0.0"
buffer-shims "~1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
string_decoder "~0.10.x"
string_decoder "~1.0.0"
util-deprecate "~1.0.1"
readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6:
readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
......@@ -4321,6 +4321,12 @@ string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
string_decoder@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.0.tgz#f06f41157b664d86069f84bdbdc9b0d8ab281667"
dependencies:
buffer-shims "~1.0.0"
stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册