Commit 54a8cbdd authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into rename-builds-controller

* upstream/master: (31 commits)
  Remove 'no changes' entries from changelog
  Check if OLD is set when migrating issue assignees
  Fix data migration from trigger schedules
  Replace EFS section in AWS guide
  Add warning about AWS EFS and performance
  Consolidate opening text from about.gitlab.com and add active/passive note
  Fix invalid object reference in ee_compat_check script
  Fix Ordered Task List Items
  Add auxiliary viewer for README
  Update fe_guide testing.md
  Add auxiliary blob viewer for CHANGELOG
  Add spec for last commit info when browsing repository files
  Show last commit for current tree on tree page
  Use same last commit widget on project homepage and tree view
  Fix unassigned checkmark
  Add missing changelog for iPython notebook rendering feature
  Convert fa-history to svg; tweak alignment
  Get rid of pluck in app/services/members/authorized_destroy_service.rb
  Removes duplicate environment variable in documentation
  Fixed spacing issues in issue sidebar
  ...
parents 226b517c c9e61fa3
...@@ -4,9 +4,6 @@ entry. ...@@ -4,9 +4,6 @@ entry.
## 9.1.4 (2017-05-12) ## 9.1.4 (2017-05-12)
- No changes.
- No changes.
- No changes.
- Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123) - Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123)
- Sort the network graph both by commit date and topographically. !11057 - Sort the network graph both by commit date and topographically. !11057
- Fix cross referencing for private and internal projects. !11243 - Fix cross referencing for private and internal projects. !11243
...@@ -56,6 +53,7 @@ entry. ...@@ -56,6 +53,7 @@ entry.
## 9.1.0 (2017-04-22) ## 9.1.0 (2017-04-22)
- Add Jupyter notebook rendering !10017
- Added merge requests empty state. !7342 - Added merge requests empty state. !7342
- Add option to start a new resolvable discussion in an MR. !7527 - Add option to start a new resolvable discussion in an MR. !7527
- Hide form inputs for group member without editing rights. !7816 - Hide form inputs for group member without editing rights. !7816
......
...@@ -114,6 +114,7 @@ export default class BlobViewer { ...@@ -114,6 +114,7 @@ export default class BlobViewer {
$(viewer).syntaxHighlight(); $(viewer).syntaxHighlight();
this.$fileHolder.trigger('highlight:line'); this.$fileHolder.trigger('highlight:line');
gl.utils.handleLocationHash();
this.toggleCopyButtonState(); this.toggleCopyButtonState();
}) })
......
...@@ -45,6 +45,12 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -45,6 +45,12 @@ gl.issueBoards.BoardSidebar = Vue.extend({
detail: { detail: {
handler () { handler () {
if (this.issue.id !== this.detail.issue.id) { if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
.each((i, el) => {
$(el).remove();
});
$('.js-issue-board-sidebar', this.$el).each((i, el) => { $('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu(); $(el).data('glDropdown').clearMenu();
}); });
......
...@@ -50,7 +50,11 @@ function UsersSelect(currentUser, els) { ...@@ -50,7 +50,11 @@ function UsersSelect(currentUser, els) {
$collapsedSidebar = $block.find('.sidebar-collapsed-user'); $collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null; selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
selectedId = $dropdown.data('selected') || selectedIdDefault; selectedId = $dropdown.data('selected');
if (selectedId === undefined) {
selectedId = selectedIdDefault;
}
const assignYourself = function () { const assignYourself = function () {
const unassignedSelected = $dropdown.closest('.selectbox') const unassignedSelected = $dropdown.closest('.selectbox')
...@@ -423,8 +427,9 @@ function UsersSelect(currentUser, els) { ...@@ -423,8 +427,9 @@ function UsersSelect(currentUser, els) {
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) { const selected = getSelected();
selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault; if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) {
this.addInput($dropdown.data('field-name'), 0, {});
} }
$el.find('.is-active').removeClass('is-active'); $el.find('.is-active').removeClass('is-active');
...@@ -432,8 +437,10 @@ function UsersSelect(currentUser, els) { ...@@ -432,8 +437,10 @@ function UsersSelect(currentUser, els) {
$el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
} }
if ($selectbox[0]) { if (selected.length > 0) {
getSelected().forEach(selectedId => highlightSelected(selectedId)); getSelected().forEach(selectedId => highlightSelected(selectedId));
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
highlightSelected(0);
} else { } else {
highlightSelected(selectedId); highlightSelected(selectedId);
} }
...@@ -444,15 +451,19 @@ function UsersSelect(currentUser, els) { ...@@ -444,15 +451,19 @@ function UsersSelect(currentUser, els) {
username = user.username ? "@" + user.username : ""; username = user.username ? "@" + user.username : "";
avatar = user.avatar_url ? user.avatar_url : false; avatar = user.avatar_url ? user.avatar_url : false;
let selected = user.id === parseInt(selectedId, 10); let selected = false;
if (this.multiSelect) { if (this.multiSelect) {
selected = getSelected().find(u => user.id === u);
const fieldName = this.fieldName; const fieldName = this.fieldName;
const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']"); const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
if (field.length) { if (field.length) {
selected = true; selected = true;
} }
} else {
selected = user.id === selectedId;
} }
img = ""; img = "";
......
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default { export default {
name: 'MRWidgetNothingToMerge', name: 'MRWidgetNothingToMerge',
props: {
mr: {
type: Object,
required: true,
},
},
data() {
return { emptyStateSVG };
},
template: ` template: `
<div class="mr-widget-body"> <div class="mr-widget-body empty-state">
<button <div class="row">
type="button" <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
class="btn btn-success btn-small" <span v-html="emptyStateSVG"></span>
disabled="true"> </div>
Merge <div class="text col-sm-7 col-sm-pull-5 col-xs-12">
</button> <span>
<span class="bold"> Merge requests are a place to propose changes you have made to a project
There is nothing to merge from source branch into target branch. and discuss those changes with others.
Please push new commits or use a different branch. </span>
</span> <p>
Interested parties can even contribute by pushing commits if they want to.
</p>
<p>
Currently there are no changes in this merge request's source branch.
Please push new commits or use a different branch.
</p>
<a
v-if="mr.newBlobPath"
:href="mr.newBlobPath"
class="btn btn-inverted btn-save">
Create file
</a>
</div>
</div>
</div> </div>
`, `,
}; };
...@@ -58,6 +58,7 @@ export default class MergeRequestStore { ...@@ -58,6 +58,7 @@ export default class MergeRequestStore {
this.statusPath = data.status_path; this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path; this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path; this.plainDiffPath = data.plain_diff_path;
this.newBlobPath = data.new_blob_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path; this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path; this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path; this.mergeActionsContentPath = data.commit_change_content_path;
......
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
.fa-chevron-down { .fa-chevron-down {
font-size: $dropdown-chevron-size; font-size: $dropdown-chevron-size;
position: relative; position: relative;
top: -3px; top: -2px;
margin-left: 5px; margin-left: 5px;
} }
......
...@@ -283,17 +283,10 @@ ...@@ -283,17 +283,10 @@
.filtered-search-history-dropdown-toggle-button { .filtered-search-history-dropdown-toggle-button {
flex: 1; flex: 1;
width: auto; width: auto;
padding-right: 10px;
border-radius: 0; border-radius: 0;
border-top: 0; border: 0;
border-left: 0;
border-bottom: 0;
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
line-height: 1;
transition: color 0.1s linear; transition: color 0.1s linear;
&:hover, &:hover,
...@@ -301,6 +294,17 @@ ...@@ -301,6 +294,17 @@
color: $gl-text-color; color: $gl-text-color;
border-color: $dropdown-input-focus-border; border-color: $dropdown-input-focus-border;
outline: none; outline: none;
svg {
fill: $gl-text-color;
}
}
svg {
height: 14px;
width: 14px;
fill: $gl-text-color-secondary;
vertical-align: middle;
} }
.dropdown-toggle-text { .dropdown-toggle-text {
...@@ -312,11 +316,6 @@ ...@@ -312,11 +316,6 @@
color: inherit; color: inherit;
} }
} }
.fa {
position: static;
}
} }
.filtered-search-history-dropdown { .filtered-search-history-dropdown {
......
...@@ -169,14 +169,14 @@ ...@@ -169,14 +169,14 @@
} }
ul.task-list { ul.task-list {
li.task-list-item { > li.task-list-item {
list-style-type: none; list-style-type: none;
position: relative; position: relative;
min-height: 22px; min-height: 22px;
padding-left: 28px; padding-left: 28px;
margin-left: 0 !important; margin-left: 0 !important;
input.task-list-item-checkbox { > input.task-list-item-checkbox {
position: absolute; position: absolute;
left: 8px; left: 8px;
top: 5px; top: 5px;
......
...@@ -195,7 +195,7 @@ ...@@ -195,7 +195,7 @@
right: 0; right: 0;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 0 20px;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
...@@ -219,6 +219,10 @@ ...@@ -219,6 +219,10 @@
} }
} }
.issuable-sidebar-header {
padding-top: 10px;
}
.assign-yourself .btn-link { .assign-yourself .btn-link {
padding-left: 0; padding-left: 0;
} }
...@@ -272,11 +276,10 @@ ...@@ -272,11 +276,10 @@
} }
width: $gutter_collapsed_width; width: $gutter_collapsed_width;
padding-top: 0; padding: 0;
.block { .block {
width: $gutter_collapsed_width - 2px; width: $gutter_collapsed_width - 2px;
margin-left: -19px;
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: none; border-bottom: none;
overflow: hidden; overflow: hidden;
......
...@@ -353,6 +353,22 @@ ...@@ -353,6 +353,22 @@
margin-top: 10px; margin-top: 10px;
margin-left: 12px; margin-left: 12px;
} }
&.empty-state {
.artwork {
margin-bottom: $gl-padding;
}
.text {
span {
font-weight: bold;
}
p {
margin-top: $gl-padding;
}
}
}
} }
.mr-widget-footer { .mr-widget-footer {
......
...@@ -639,36 +639,6 @@ pre.light-well { ...@@ -639,36 +639,6 @@ pre.light-well {
} }
} }
.project-last-commit {
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-base;
padding: 12px;
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
}
.ci-status {
margin-right: $gl-padding;
}
.commit-row-message {
color: $gl-text-color;
}
.commit-sha {
margin-right: 5px;
font-weight: 600;
}
.commit-author-link {
.commit-author-name {
font-weight: 600;
}
}
}
.project-show-readme { .project-show-readme {
.row-content-block { .row-content-block {
background-color: inherit; background-color: inherit;
......
...@@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
render 'show' render 'show'
end end
......
...@@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController
end end
end end
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
respond_to do |format| respond_to do |format|
format.html format.html
# Disable cache so browser history works # Disable cache so browser history works
......
...@@ -278,4 +278,19 @@ module BlobHelper ...@@ -278,4 +278,19 @@ module BlobHelper
options options
end end
def contribution_options(project)
options = []
if can?(current_user, :create_issue, project)
options << link_to("submit an issue", new_namespace_project_issue_path(project.namespace, project))
end
merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project))
if merge_project
options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project))
end
options
end
end end
...@@ -91,7 +91,7 @@ module CommitsHelper ...@@ -91,7 +91,7 @@ module CommitsHelper
end end
def link_to_browse_code(project, commit) def link_to_browse_code(project, commit)
return unless current_controller?(:projects, :commits) return unless current_controller?(:commits)
if @path.blank? if @path.blank?
return link_to( return link_to(
......
...@@ -39,7 +39,11 @@ class Blob < SimpleDelegator ...@@ -39,7 +39,11 @@ class Blob < SimpleDelegator
AUXILIARY_VIEWERS = [ AUXILIARY_VIEWERS = [
BlobViewer::GitlabCiYml, BlobViewer::GitlabCiYml,
BlobViewer::RouteMap, BlobViewer::RouteMap,
BlobViewer::License
BlobViewer::Readme,
BlobViewer::License,
BlobViewer::Contributing,
BlobViewer::Changelog
].freeze ].freeze
attr_reader :project attr_reader :project
......
...@@ -2,11 +2,17 @@ module BlobViewer ...@@ -2,11 +2,17 @@ module BlobViewer
module Auxiliary module Auxiliary
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::Allowable
included do included do
self.loading_partial_name = 'loading_auxiliary' self.loading_partial_name = 'loading_auxiliary'
self.type = :auxiliary self.type = :auxiliary
self.overridable_max_size = 100.kilobytes self.overridable_max_size = 100.kilobytes
self.max_size = 100.kilobytes self.max_size = 100.kilobytes
end end
def visible_to?(current_user)
true
end
end end
end end
...@@ -11,6 +11,8 @@ module BlobViewer ...@@ -11,6 +11,8 @@ module BlobViewer
attr_reader :blob attr_reader :blob
attr_accessor :override_max_size attr_accessor :override_max_size
delegate :project, to: :blob
def initialize(blob) def initialize(blob)
@blob = blob @blob = blob
end end
......
module BlobViewer
class Changelog < Base
include Auxiliary
include Static
self.partial_name = 'changelog'
self.file_types = %i(changelog)
self.binary = false
def render_error
return if project.repository.tag_count > 0
:no_tags
end
end
end
module BlobViewer
class Contributing < Base
include Auxiliary
include Static
self.partial_name = 'contributing'
self.file_types = %i(contributing)
self.binary = false
end
end
...@@ -8,7 +8,7 @@ module BlobViewer ...@@ -8,7 +8,7 @@ module BlobViewer
self.binary = false self.binary = false
def license def license
blob.project.repository.license project.repository.license
end end
def render_error def render_error
......
module BlobViewer
class Readme < Base
include Auxiliary
include Static
self.partial_name = 'readme'
self.file_types = %i(readme)
self.binary = false
def visible_to?(current_user)
can?(current_user, :read_wiki, project)
end
end
end
...@@ -930,10 +930,18 @@ class User < ActiveRecord::Base ...@@ -930,10 +930,18 @@ class User < ActiveRecord::Base
end end
def invalidate_cache_counts def invalidate_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) invalidate_issue_cache_counts
invalidate_merge_request_cache_counts
end
def invalidate_issue_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_issues_count']) Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
end end
def invalidate_merge_request_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
end
def todos_done_count(force: false) def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
TodosFinder.new(self, state: :done).execute.count TodosFinder.new(self, state: :done).execute.count
......
...@@ -97,6 +97,14 @@ class MergeRequestEntity < IssuableEntity ...@@ -97,6 +97,14 @@ class MergeRequestEntity < IssuableEntity
presenter(merge_request).target_branch_commits_path presenter(merge_request).target_branch_commits_path
end end
expose :new_blob_path do |merge_request|
if can?(current_user, :push_code, merge_request.project)
namespace_project_new_blob_path(merge_request.project.namespace,
merge_request.project,
merge_request.source_branch)
end
end
expose :conflict_resolution_path do |merge_request| expose :conflict_resolution_path do |merge_request|
presenter(merge_request).conflict_resolution_path presenter(merge_request).conflict_resolution_path
end end
......
...@@ -179,6 +179,7 @@ class IssuableBaseService < BaseService ...@@ -179,6 +179,7 @@ class IssuableBaseService < BaseService
issuable.create_cross_references!(current_user) issuable.create_cross_references!(current_user)
execute_hooks(issuable) execute_hooks(issuable)
issuable.assignees.each(&:invalidate_cache_counts) issuable.assignees.each(&:invalidate_cache_counts)
invalidate_cache_counts(issuable.assignees, issuable)
end end
issuable issuable
...@@ -237,7 +238,7 @@ class IssuableBaseService < BaseService ...@@ -237,7 +238,7 @@ class IssuableBaseService < BaseService
if old_assignees != issuable.assignees if old_assignees != issuable.assignees
assignees = old_assignees + issuable.assignees.to_a assignees = old_assignees + issuable.assignees.to_a
assignees.compact.each(&:invalidate_cache_counts) invalidate_cache_counts(assignees.compact, issuable)
end end
after_update(issuable) after_update(issuable)
...@@ -330,4 +331,10 @@ class IssuableBaseService < BaseService ...@@ -330,4 +331,10 @@ class IssuableBaseService < BaseService
create_labels_note(issuable, old_labels) if issuable.labels != old_labels create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end end
def invalidate_cache_counts(users, issuable)
users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts")
end
end
end end
...@@ -26,10 +26,14 @@ module Members ...@@ -26,10 +26,14 @@ module Members
def unassign_issues_and_merge_requests(member) def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember) if member.is_a?(GroupMember)
issue_ids = IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). issues = Issue.unscoped.select(1).
execute.pluck(:id) joins(:project).
where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
IssueAssignee.delete_all(issue_id: issue_ids, user_id: member.user_id) # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped.
where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues).
delete_all
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute. execute.
......
...@@ -16,24 +16,15 @@ ...@@ -16,24 +16,15 @@
= icon('spinner') = icon('spinner')
Reset health check access token Reset health check access token
%p.light %p.light
Health information can be retrieved as plain text, JSON, or XML using: Health information can be retrieved from the following endpoints. More information is available
= link_to 'here', help_page_path('user/admin_area/monitoring/health_check')
%ul %ul
%li %li
%code= health_check_url(token: current_application_settings.health_check_access_token) %code= readiness_url(token: current_application_settings.health_check_access_token)
%li %li
%code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) %code= liveness_url(token: current_application_settings.health_check_access_token)
%li %li
%code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) %code= metrics_url(token: current_application_settings.health_check_access_token)
%p.light
You can also ask for the status of specific services:
%ul
%li
%code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache)
%li
%code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database)
%li
%code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
%hr %hr
.panel.panel-default .panel.panel-default
......
- commit = local_assigns.fetch(:commit) { @repository.commit }
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
#tree-holder.tree-holder.clearfix #tree-holder.tree-holder.clearfix
.nav-block .nav-block
= render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree - if commit
.info-well.hidden-xs.project-last-commit.append-bottom-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: commit, ref: ref, project: project
= render 'projects/tree/tree_content', tree: @tree
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- if status
= link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
= ci_text_for_status(status)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot;
#{time_ago_with_tooltip(commit.committed_date)} by
= commit_author_link(commit, avatar: true, size: 24)
- blob = local_assigns.fetch(:blob)
- auxiliary_viewer = blob.auxiliary_viewer
- if auxiliary_viewer && auxiliary_viewer.render_error.nil? && auxiliary_viewer.visible_to?(current_user)
.well-segment.blob-auxiliary-viewer
= render 'projects/blob/viewer', viewer: auxiliary_viewer
...@@ -3,13 +3,9 @@ ...@@ -3,13 +3,9 @@
.info-well.hidden-xs .info-well.hidden-xs
.well-segment .well-segment
%ul.blob-commit-info %ul.blob-commit-info
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
= render blob_commit, project: @project, ref: @ref
- auxiliary_viewer = blob.auxiliary_viewer = render "projects/blob/auxiliary_viewer", blob: blob
- if auxiliary_viewer && !auxiliary_viewer.render_error
.well-segment.blob-auxiliary-viewer
= render 'projects/blob/viewer', viewer: auxiliary_viewer
#blob-content-holder.blob-content-holder #blob-content-holder.blob-content-holder
%article.file-holder %article.file-holder
......
= icon('history fw')
= succeed '.' do
To find the state of this project's repository at the time of any of these versions, check out
= link_to "the tags", namespace_project_tags_path(viewer.project.namespace, viewer.project)
= icon('book fw')
After you've reviewed these contribution guidelines, you'll be all set to
- options = contribution_options(viewer.project)
- if options.any?
= succeed '.' do
= options.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
- else
contribute to this project.
= icon('info-circle fw')
= succeed '.' do
To learn more about this project, read
= link_to "the wiki", namespace_project_wikis_path(viewer.project.namespace, viewer.project)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_issue, @project)
.selectbox.hide-collapsed .selectbox.hide-collapsed
%input{ type: "hidden", %input.js-vue{ type: "hidden",
name: "issue[assignee_ids][]", name: "issue[assignee_ids][]",
":value" => "assignee.id", ":value" => "assignee.id",
"v-if" => "issue.assignees", "v-if" => "issue.assignees",
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } }, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee Select assignee
= icon("chevron-down") = icon("chevron-down")
......
...@@ -73,11 +73,6 @@ ...@@ -73,11 +73,6 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
Set up auto deploy Set up auto deploy
- if @repository.commit
%div{ class: container_class }
.project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
%div{ class: container_class } %div{ class: container_class }
- if @project.archived? - if @project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
......
...@@ -7,13 +7,4 @@ ...@@ -7,13 +7,4 @@
= render 'projects/last_push' = render 'projects/last_push'
%div{ class: container_class } %div{ class: container_class }
#tree-holder.tree-holder.clearfix = render 'projects/files', commit: @last_commit, project: @project, ref: @ref
.nav-block
= render 'projects/tree/tree_header', tree: @tree
.info-well.hidden-xs.append-bottom-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @commit, project: @project, ref: @ref
= render 'projects/tree/tree_content', tree: @tree
<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792"><path d="M1664 896q0 156-61 298t-164 245-245 164-298 61q-172 0-327-72.5T305 1387q-7-10-6.5-22.5t8.5-20.5l137-138q10-9 25-9 16 2 23 12 73 95 179 147t225 52q104 0 198.5-40.5T1258 1258t109.5-163.5T1408 896t-40.5-198.5T1258 534t-163.5-109.5T896 384q-98 0-188 35.5T548 521l137 138q31 30 14 69-17 40-59 40H192q-26 0-45-19t-19-45V256q0-42 40-59 39-17 69 14l130 129q107-101 244.5-156.5T896 128q156 0 298 61t245 164 164 245 61 298zm-640-288v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V608q0-14 9-23t23-9h64q14 0 23 9t9 23z"/></svg>
<svg width="256" height="146" viewBox="0 0 256 146" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>illustration</title><defs><rect id="a" width="178.714" height="115.389" rx="10"/><mask id="d" x="0" y="0" width="178.714" height="115.389" fill="#fff"><use xlink:href="#a"/></mask><path d="M8.796 31.515c.395.047.8.072 1.207.072h23.065c5.536 0 10.003-4.475 10.003-9.994v-11.6C43.07 4.476 38.594 0 33.07 0H10.003C4.467 0 0 4.475 0 9.994v11.6c0 1.248.23 2.444.65 3.547H0v7.414c0 4.094 2.394 5.113 5.342 2.28l3.454-3.32z" id="b"/><mask id="e" x="0" y="0" width="43.071" height="36.437" fill="#fff"><use xlink:href="#b"/></mask><path d="M8.796 31.515c.395.047.8.072 1.207.072h23.065c5.536 0 10.003-4.475 10.003-9.994v-11.6C43.07 4.476 38.594 0 33.07 0H10.003C4.467 0 0 4.475 0 9.994v11.6c0 1.248.23 2.444.65 3.547H0v7.414c0 4.094 2.394 5.113 5.342 2.28l3.454-3.32z" id="c"/><mask id="f" x="0" y="0" width="43.071" height="36.437" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.868)" fill="#F9F9F9"><rect x="19.286" width="77.143" height="14.182" rx="7.091"/><rect y="28.364" width="84.857" height="14.182" rx="7.091"/><rect x="133.714" y="42.546" width="122.143" height="14.182" rx="7.091"/><rect x="82.929" y="126.992" width="101.571" height="14.182" rx="7.091"/><rect x="42.429" y="99.273" width="101.571" height="14.182" rx="7.091"/><rect x="19.929" y="70.909" width="225" height="14.182" rx="7.091"/><path d="M98.37 14.182H13.488h13.81a7.098 7.098 0 0 1 7.094 7.09 7.09 7.09 0 0 1-7.094 7.092h-13.81 84.88-23.452a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.096-7.092h23.452zm162 42.545h-75.238 23.452a7.098 7.098 0 0 1 7.095 7.09 7.09 7.09 0 0 1-7.096 7.092h-23.452 75.237-23.453a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.095-7.093h23.452zM103.512 85.09H28.275h23.452a7.098 7.098 0 0 1 7.095 7.092 7.09 7.09 0 0 1-7.095 7.09H28.275h75.237H80.06a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.095-7.09h23.452zm48.215 28.365H76.49 90.3a7.098 7.098 0 0 1 7.093 7.09 7.09 7.09 0 0 1-7.094 7.092H76.49h75.237-33.096a7.098 7.098 0 0 1-7.094-7.09 7.09 7.09 0 0 1 7.095-7.092h33.097z"/></g><g transform="translate(38.57 12.248)"><use stroke="#EEE" mask="url(#d)" stroke-width="8" fill="#FFF" xlink:href="#a"/><path fill="#EEE" d="M2.57 18.694h174.215v2.58H2.57z"/><g transform="translate(21.857 38.678)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(21.857 59.95)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#FC6D26" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(21.857 81.223)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(100.93 38.033)"><rect fill="#FDE5D8" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="14.826" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="21.917" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" y="21.273" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="37.286" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="25.071" y="35.455" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="18.643" y="28.364" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="30.857" y="21.273" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="35.455" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="21.857" y="21.273" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="30.857" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="39.857" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="49.5" y="14.182" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="29.008" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="36.099" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="43.19" width="3.857" height="1.289" rx=".645"/><rect fill="#6B4FBB" x="9.643" y="42.546" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="56.727" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="34.071" y="49.636" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="56.727" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="18.643" y="49.636" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="21.857" y="42.546" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" y="49.636" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="49.636" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="50.281" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="57.372" width="3.857" height="1.289" rx=".645"/></g></g><g transform="translate(196.07)"><use stroke="#FDE5D8" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#b"/><rect fill="#FDB692" x="9" y="9.025" width="18.643" height="1.934" rx=".967"/><rect fill="#FDB692" x="9" y="14.826" width="25.071" height="1.934" rx=".967"/><rect fill="#FDB692" x="9" y="20.628" width="18.643" height="1.934" rx=".967"/></g><g transform="translate(189 41.256)"><ellipse stroke="#FC6D26" stroke-width="3" fill="#FFF7F4" cx="10.286" cy="9.669" rx="9.643" ry="9.669"/><path d="M.023 9.002a8.352 8.352 0 0 0 7.94-4.308M9 .644c0-.21-.008-.416-.023-.62" stroke="#FC6D26" stroke-width="2"/><path d="M5.045 2.008A10.266 10.266 0 0 0 13.5 6.446c2.112 0 4.076-.638 5.71-1.733" stroke="#FC6D26" stroke-width="2"/><ellipse fill="#FC6D26" cx="6.75" cy="11.281" rx=".964" ry=".967"/><ellipse fill="#FC6D26" cx="13.821" cy="11.281" rx=".964" ry=".967"/></g><g transform="translate(46.93 96.05)"><ellipse stroke="#6B4FBB" stroke-width="3" fill="#F4F1FA" cx="9.643" cy="10.314" rx="9.643" ry="9.669"/><path d="M12.86 4.51h-.005L11.25 2.58 9.645 4.51H9.64L8.036 2.58 6.43 4.51h-.002L4.82 2.58 3.215 4.512h-1.75A9.646 9.646 0 0 1 9.642 0c3.447 0 6.47 1.8 8.176 4.508h-1.75l-1.605-1.93L12.86 4.51z" fill="#6B4FBB"/><ellipse fill="#6B4FBB" cx="6.107" cy="11.281" rx=".964" ry=".967"/><ellipse fill="#6B4FBB" cx="13.179" cy="11.281" rx=".964" ry=".967"/></g><g transform="matrix(-1 0 0 1 56.57 54.794)"><use stroke="#E2DCF2" mask="url(#f)" stroke-width="8" fill="#FFF" xlink:href="#c"/><rect fill="#6B4FBB" opacity=".5" x="15.429" y="9.025" width="18.643" height="1.934" rx=".967"/><rect fill="#6B4FBB" opacity=".5" x="21.857" y="14.826" width="12.214" height="1.934" rx=".967"/><rect fill="#6B4FBB" opacity=".5" x="21.857" y="20.628" width="12.214" height="1.934" rx=".967"/></g></g></svg>
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.issues-other-filters.filtered-search-wrapper .issues-other-filters.filtered-search-wrapper
.filtered-search-box .filtered-search-box
- if type != :boards_modal && type != :boards - if type != :boards_modal && type != :boards
= dropdown_tag(content_tag(:i, '', class: 'fa fa-history'), = dropdown_tag(custom_icon('icon_history'),
options: { wrapper_class: "filtered-search-history-dropdown-wrapper", options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "filtered-search-history-dropdown-toggle-button", toggle_class: "filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown", dropdown_class: "filtered-search-history-dropdown",
......
---
title: Fix Ordered Task List Items
merge_request: 31483
author: Jared Deckard <jared.deckard@gmail.com>
---
title: Removes duplicate environment variable in documentation
merge_request:
author:
---
title: Invalidate cache for issue and MR counters more granularly
merge_request:
author:
---
title: Show last commit for current tree on tree page
merge_request:
author:
---
title: Issue assignees are now removed without loading unnecessary data into memory
merge_request:
author:
---
title: Added application readiness endpoints to the monitoring health check admin
view
merge_request:
author:
...@@ -47,7 +47,7 @@ class MigrateAssigneeToSeparateTable < ActiveRecord::Migration ...@@ -47,7 +47,7 @@ class MigrateAssigneeToSeparateTable < ActiveRecord::Migration
RETURNS trigger AS RETURNS trigger AS
$BODY$ $BODY$
BEGIN BEGIN
if OLD.assignee_id IS NOT NULL THEN if OLD IS NOT NULL AND OLD.assignee_id IS NOT NULL THEN
DELETE FROM issue_assignees WHERE issue_id = OLD.id; DELETE FROM issue_assignees WHERE issue_id = OLD.id;
END IF; END IF;
......
...@@ -4,6 +4,13 @@ class MigrateTriggerSchedulesToPipelineSchedules < ActiveRecord::Migration ...@@ -4,6 +4,13 @@ class MigrateTriggerSchedulesToPipelineSchedules < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
connection.execute <<~SQL
DELETE FROM ci_trigger_schedules WHERE NOT EXISTS
(SELECT true FROM projects
WHERE ci_trigger_schedules.project_id = projects.id
)
SQL
connection.execute <<-SQL connection.execute <<-SQL
INSERT INTO ci_pipeline_schedules ( INSERT INTO ci_pipeline_schedules (
project_id, project_id,
......
...@@ -20,7 +20,6 @@ Variable | Type | Description ...@@ -20,7 +20,6 @@ Variable | Type | Description
`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab `GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab `GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab `GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab `GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer `GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer `GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
......
...@@ -5,6 +5,20 @@ The solution you choose will be based on the level of scalability and ...@@ -5,6 +5,20 @@ The solution you choose will be based on the level of scalability and
availability you require. The easiest solutions are scalable, but not necessarily availability you require. The easiest solutions are scalable, but not necessarily
highly available. highly available.
GitLab provides a service that is usually essential to most organizations: it
enables people to collaborate on code in a timely fashion. Any downtime should
therefore be short and planned. Luckily, GitLab provides a solid setup even on
a single server without special measures. Due to the distributed nature
of Git, developers can still commit code locally even when GitLab is not
available. However, some GitLab features such as the issue tracker and
Continuous Integration are not available when GitLab is down.
**Keep in mind that all Highly Available solutions come with a trade-off between
cost/complexity and uptime**. The more uptime you want, the more complex the
solution. And the more complex the solution, the more work is involved in
setting up and maintaining it. High availability is not free and every HA
solution should balance the costs against the benefits.
## Architecture ## Architecture
There are two kinds of setups: There are two kinds of setups:
...@@ -37,6 +51,10 @@ Block Device) to keep all data in sync. DRBD requires a low latency link to ...@@ -37,6 +51,10 @@ Block Device) to keep all data in sync. DRBD requires a low latency link to
remain in sync. It is not advisable to attempt to run DRBD between data centers remain in sync. It is not advisable to attempt to run DRBD between data centers
or in different cloud availability zones. or in different cloud availability zones.
> **Note:** GitLab recommends against choosing this HA method because of the
complexity of managing DRBD and crafting automatic failover. This is
*compatible* with GitLab, but not officially *supported*.
Components/Servers Required: 2 servers/virtual machines (one active/one passive) Components/Servers Required: 2 servers/virtual machines (one active/one passive)
![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png) ![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
...@@ -7,6 +7,25 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as ...@@ -7,6 +7,25 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
specifically test NFSv3. specifically test NFSv3.
## AWS Elastic File System
GitLab does not recommend using AWS Elastic File System (EFS).
Customers and users have reported that AWS EFS does not perform well for GitLab's
use-case. There are several issues that can cause problems. For these reasons
GitLab does not recommend using EFS with GitLab.
- EFS bases allowed IOPS on volume size. The larger the volume, the more IOPS
are allocated. For smaller volumes, users may experience decent performance
for a period of time due to 'Burst Credits'. Over a period of weeks to months
credits may run out and performance will bottom out.
- For larger volumes, allocated IOPS may not be the problem. Workloads where
many small files are written in a serialized manner are not well-suited for EFS.
EBS with an NFS server on top will perform much better.
For more details on another person's experience with EFS, see
[Amazon's Elastic File System: Burst Credits](https://www.rawkode.io/2017/04/amazons-elastic-file-system-burst-credits/)
### Recommended options ### Recommended options
When you define your NFS exports, we recommend you also add the following When you define your NFS exports, we recommend you also add the following
......
# Frontend Testing # Frontend Testing
There are two types of tests you'll encounter while developing frontend code There are two types of test suites you'll encounter while developing frontend code
at GitLab. We use Karma and Jasmine for JavaScript unit testing, and RSpec at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, and RSpec
feature tests with Capybara for integration testing. feature tests with Capybara for e2e (end-to-end) integration testing.
Feature tests need to be written for all new features. Regression tests ought Unit and feature tests need to be written for all new features.
to be written for all bug fixes to prevent them from recurring in the future. Most of the time, you should use rspec for your feature tests.
There are cases where the behaviour you are testing is not worth the time spent running the full application,
for example, if you are testing styling, animation or small actions that don't involve the backend,
you should write an integration test using Jasmine.
![Testing priority triangle](img/testing_triangle.png)
_This diagram demonstrates the relative priority of each test type we use_
Regression tests should be written for bug fixes to prevent them from recurring in the future.
See [the Testing Standards and Style Guidelines](../testing.md) See [the Testing Standards and Style Guidelines](../testing.md)
for more information on general testing practices at GitLab. for more information on general testing practices at GitLab.
...@@ -13,10 +22,12 @@ for more information on general testing practices at GitLab. ...@@ -13,10 +22,12 @@ for more information on general testing practices at GitLab.
## Karma test suite ## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
framework for our JavaScript unit tests. For tests that rely on DOM framework for our JavaScript unit and integration tests. For integration tests,
manipulation, we generate HTML files using RSpec suites (see `spec/javascripts/fixtures/*.rb` for examples). we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples).
Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`). Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
Those will be migrated over time. Adding these static fixtures should be avoided as they are harder to keep up to date with real views.
The existing static fixtures will be migrated over time.
Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress.
Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin. Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
JavaScript tests live in `spec/javascripts/`, matching the folder structure JavaScript tests live in `spec/javascripts/`, matching the folder structure
...@@ -28,7 +39,9 @@ browser and you will not have access to certain APIs, such as ...@@ -28,7 +39,9 @@ browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed. which will have to be stubbed.
### Writing tests ### Best practice
#### Naming unit tests
When writing describe test blocks to test specific functions/methods, When writing describe test blocks to test specific functions/methods,
please use the method name as the describe block name. please use the method name as the describe block name.
...@@ -56,6 +69,14 @@ describe('.methodName', () => { ...@@ -56,6 +69,14 @@ describe('.methodName', () => {
}); });
``` ```
#### Stubbing
For unit tests, you should stub methods that are unrelated to the current unit you are testing.
If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
For integration tests, you should stub methods that will effect the stability of the test if they
execute their original behaviour. i.e. Network requests.
### Vue.js unit tests ### Vue.js unit tests
See this [section][vue-test]. See this [section][vue-test].
......
...@@ -159,19 +159,21 @@ subnet and security group and ...@@ -159,19 +159,21 @@ subnet and security group and
*** ***
## Elastic File System ## Network File System
This new AWS offering allows us to create a file system accessible by
 GitLab requires a shared filesystem such as NFS. The file share(s) will be
EC2 instances within a VPC. Choose our VPC and the subnets will be mounted on all application servers. There are a variety of ways to build an

automatically configured assuming we don't need to set explicit IPs. NFS server on AWS.
The
next section allows us to add tags and choose between General
Purpose or
Max I/O which is a good option when being accessed by a
large number of
EC2 instances.


![Elastic File System](img/elastic-file-system.png) One option is to use a third-party AMI that offers NFS as a service. A [search
for 'NFS' in the AWS Marketplace](https://aws.amazon.com/marketplace/search/results?x=0&y=0&searchTerms=NFS&page=1&ref_=nav_search_box)
shows options such as NetApp, SoftNAS and others.
To actually mount and install the NFS client we'll use the User Data Another option is to build a simple NFS server using a vanilla Linux server backed
section when adding our Launch Configuration. by AWS Elastic Block Storage (EBS).
> **Note:** GitLab does not recommend using AWS Elastic File System (EFS). See
details in [High Availability NFS documentation](../../../administration/high_availability/nfs.md#aws-elastic-file-system)
*** ***
......
...@@ -256,9 +256,9 @@ module SharedProject ...@@ -256,9 +256,9 @@ module SharedProject
end end
step 'I should see last commit with CI status' do step 'I should see last commit with CI status' do
page.within ".project-last-commit" do page.within ".blob-commit-info" do
expect(page).to have_content(project.commit.sha[0..6]) expect(page).to have_content(project.commit.sha[0..6])
expect(page).to have_content("skipped") expect(page).to have_link("Commit: skipped")
end end
end end
......
...@@ -131,10 +131,12 @@ module Gitlab ...@@ -131,10 +131,12 @@ module Gitlab
def check_patch(patch_path) def check_patch(patch_path)
step("Checking out master", %w[git checkout master]) step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master]) step("Resetting to latest master", %w[git reset --hard origin/master])
step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}])
step( step(
"Checking if #{patch_path} applies cleanly to EE/master", "Checking if #{patch_path} applies cleanly to EE/master",
%W[git apply --check --3way #{patch_path}] %W[git apply --check --3way #{patch_path}]
) do |output, status| ) do |output, status|
puts output
unless status.zero? unless status.zero?
@failed_files = output.lines.reduce([]) do |memo, line| @failed_files = output.lines.reduce([]) do |memo, line|
if line.start_with?('error: patch failed:') if line.start_with?('error: patch failed:')
...@@ -309,12 +311,12 @@ module Gitlab ...@@ -309,12 +311,12 @@ module Gitlab
U lib/gitlab/ee_compat_check.rb U lib/gitlab/ee_compat_check.rb
Resolve them, stage the changes and commit them. Resolve them, stage the changes and commit them.
If the patch couldn't be applied cleanly, use the following command: If the patch couldn't be applied cleanly, use the following command:
# In the EE repo # In the EE repo
$ git apply --reject path/to/#{ce_branch}.patch $ git apply --reject path/to/#{ce_branch}.patch
This option makes git apply the parts of the patch that are applicable, This option makes git apply the parts of the patch that are applicable,
and leave the rejected hunks in corresponding `.rej` files. and leave the rejected hunks in corresponding `.rej` files.
You can then resolve the conflicts highlighted in `.rej` by You can then resolve the conflicts highlighted in `.rej` by
......
...@@ -26,9 +26,20 @@ RSpec.describe 'Dashboard Issues', feature: true do ...@@ -26,9 +26,20 @@ RSpec.describe 'Dashboard Issues', feature: true do
expect(page).not_to have_content(other_issue.title) expect(page).not_to have_content(other_issue.title)
end end
it 'shows checkmark when unassigned is selected for assignee', js: true do
find('.js-assignee-search').click
find('li', text: 'Unassigned').click
find('.js-assignee-search').click
expect(find('li[data-user-id="0"] a.is-active')).to be_visible
end
it 'shows issues when current user is author', js: true do it 'shows issues when current user is author', js: true do
find('#assignee_id', visible: false).set('') find('#assignee_id', visible: false).set('')
find('.js-author-search', match: :first).click find('.js-author-search', match: :first).click
expect(find('li[data-user-id="null"] a.is-active')).to be_visible
find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
find('.js-author-search', match: :first).click find('.js-author-search', match: :first).click
......
...@@ -31,4 +31,16 @@ feature 'user browses project', feature: true, js: true do ...@@ -31,4 +31,16 @@ feature 'user browses project', feature: true, js: true do
expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
expect(page).to have_content 'size 1575078' expect(page).to have_content 'size 1575078'
end end
scenario 'can see last commit for current directory' do
last_commit = project.repository.last_commit_for_path(project.default_branch, 'files')
click_link 'files'
wait_for_ajax
page.within('.blob-commit-info') do
expect(page).to have_content last_commit.short_id
expect(page).to have_content last_commit.author_name
end
end
end end
...@@ -86,6 +86,7 @@ ...@@ -86,6 +86,7 @@
"email_patches_path": { "type": "string" }, "email_patches_path": { "type": "string" },
"plain_diff_path": { "type": "string" }, "plain_diff_path": { "type": "string" },
"status_path": { "type": "string" }, "status_path": { "type": "string" },
"new_blob_path": { "type": "string" },
"merge_check_path": { "type": "string" }, "merge_check_path": { "type": "string" },
"ci_environments_status_path": { "type": "string" }, "ci_environments_status_path": { "type": "string" },
"merge_commit_message_with_description": { "type": "string" }, "merge_commit_message_with_description": { "type": "string" },
......
...@@ -4,14 +4,26 @@ import nothingToMergeComponent from '~/vue_merge_request_widget/components/state ...@@ -4,14 +4,26 @@ import nothingToMergeComponent from '~/vue_merge_request_widget/components/state
describe('MRWidgetNothingToMerge', () => { describe('MRWidgetNothingToMerge', () => {
describe('template', () => { describe('template', () => {
const Component = Vue.extend(nothingToMergeComponent); const Component = Vue.extend(nothingToMergeComponent);
const newBlobPath = '/foo';
const vm = new Component({ const vm = new Component({
el: document.createElement('div'), el: document.createElement('div'),
propsData: {
mr: { newBlobPath },
},
}); });
it('should have correct elements', () => { it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(vm.$el.querySelector('a').href).toContain(newBlobPath);
expect(vm.$el.innerText).toContain('There is nothing to merge from source branch into target branch.'); expect(vm.$el.innerText).toContain('Currently there are no changes in this merge request\'s source branch');
expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.'); expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.');
}); });
it('should not show new blob link if there is no link available', () => {
vm.mr.newBlobPath = null;
Vue.nextTick(() => {
expect(vm.$el.querySelector('a')).toEqual(null);
});
});
}); });
}); });
require 'spec_helper'
describe BlobViewer::Changelog, model: true do
include FakeBlobHelpers
let(:project) { create(:project, :repository) }
let(:blob) { fake_blob(path: 'CHANGELOG') }
subject { described_class.new(blob) }
describe '#render_error' do
context 'when there are no tags' do
before do
allow(project.repository).to receive(:tag_count).and_return(0)
end
it 'returns :no_tags' do
expect(subject.render_error).to eq(:no_tags)
end
end
context 'when there are tags' do
it 'returns nil' do
expect(subject.render_error).to be_nil
end
end
end
end
...@@ -1777,4 +1777,32 @@ describe User, models: true do ...@@ -1777,4 +1777,32 @@ describe User, models: true do
expect(user.preferred_language).to eq('en') expect(user.preferred_language).to eq('en')
end end
end end
context '#invalidate_issue_cache_counts' do
let(:user) { build_stubbed(:user) }
it 'invalidates cache for issue counter' do
cache_mock = double
expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
allow(Rails).to receive(:cache).and_return(cache_mock)
user.invalidate_issue_cache_counts
end
end
context '#invalidate_merge_request_cache_counts' do
let(:user) { build_stubbed(:user) }
it 'invalidates cache for Merge Request counter' do
cache_mock = double
expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count'])
allow(Rails).to receive(:cache).and_return(cache_mock)
user.invalidate_merge_request_cache_counts
end
end
end end
...@@ -65,6 +65,23 @@ describe MergeRequestEntity do ...@@ -65,6 +65,23 @@ describe MergeRequestEntity do
.to eq(resource.merge_commit_message(include_description: true)) .to eq(resource.merge_commit_message(include_description: true))
end end
describe 'new_blob_path' do
context 'when user can push to project' do
it 'returns path' do
project.add_developer(user)
expect(subject[:new_blob_path])
.to eq("/#{resource.project.full_path}/new/#{resource.source_branch}")
end
end
context 'when user cannot push to project' do
it 'returns nil' do
expect(subject[:new_blob_path]).to be_nil
end
end
end
describe 'diff_head_sha' do describe 'diff_head_sha' do
before do before do
allow(resource).to receive(:diff_head_sha) { 'sha' } allow(resource).to receive(:diff_head_sha) { 'sha' }
......
require 'spec_helper'
describe 'projects/_last_commit', :view do
let(:project) { create(:project, :repository) }
context 'when there is a pipeline present for the commit' do
context 'when pipeline is blocked' do
let!(:pipeline) do
create(:ci_pipeline, :blocked, project: project,
sha: project.commit.id)
end
it 'shows correct pipeline badge' do
render 'projects/last_commit', commit: project.commit,
project: project,
ref: :master
expect(rendered).to have_text "blocked #{project.commit.short_id}"
end
end
end
end
...@@ -21,11 +21,11 @@ describe 'projects/tree/show' do ...@@ -21,11 +21,11 @@ describe 'projects/tree/show' do
let(:tree) { repository.tree(commit.id, path) } let(:tree) { repository.tree(commit.id, path) }
before do before do
assign(:id, File.join(ref, path))
assign(:ref, ref) assign(:ref, ref)
assign(:commit, commit)
assign(:id, commit.id)
assign(:tree, tree)
assign(:path, path) assign(:path, path)
assign(:last_commit, commit)
assign(:tree, tree)
end end
it 'displays correctly' do it 'displays correctly' do
......
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