Commit 65b4627d authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into feature/sm/35954-create-kubernetes-cluster-on-gke-from-k8s-service

parents 6d4e2829 bd970a51
...@@ -63,4 +63,5 @@ eslint-report.html ...@@ -63,4 +63,5 @@ eslint-report.html
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec /.rspec
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.38.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -273,7 +273,7 @@ GEM ...@@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.38.0) gitaly-proto (0.39.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1027,7 +1027,7 @@ DEPENDENCIES ...@@ -1027,7 +1027,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.38.0) gitaly-proto (~> 0.39.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
...@@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either ...@@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version the version currently running on GitLab.com, or the most recent version
available in the package repositories. available in the package repositories.
A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc)
and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc),
just like any other issue, to help GitLab team members focus on issues that are
relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on).
## Release retrospective and kickoff ## Release retrospective and kickoff
- [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective) - [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective)
......
...@@ -298,7 +298,7 @@ class CopyAsGFM { ...@@ -298,7 +298,7 @@ class CopyAsGFM {
const documentFragment = getSelectedFragment(); const documentFragment = getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) return;
const el = transformer(documentFragment.cloneNode(true)); const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
if (!el) return; if (!el) return;
e.preventDefault(); e.preventDefault();
...@@ -338,55 +338,64 @@ class CopyAsGFM { ...@@ -338,55 +338,64 @@ class CopyAsGFM {
} }
static transformGFMSelection(documentFragment) { static transformGFMSelection(documentFragment) {
const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
switch (gfmEls.length) { switch (gfmElements.length) {
case 0: { case 0: {
return documentFragment; return documentFragment;
} }
case 1: { case 1: {
return gfmEls[0]; return gfmElements[0];
} }
default: { default: {
const allGfmEl = document.createElement('div'); const allGfmElement = document.createElement('div');
for (let i = 0; i < gfmEls.length; i += 1) { for (let i = 0; i < gfmElements.length; i += 1) {
const lineEl = gfmEls[i]; const gfmElement = gfmElements[i];
allGfmEl.appendChild(lineEl); allGfmElement.appendChild(gfmElement);
allGfmEl.appendChild(document.createTextNode('\n\n')); allGfmElement.appendChild(document.createTextNode('\n\n'));
} }
return allGfmEl; return allGfmElement;
} }
} }
} }
static transformCodeSelection(documentFragment) { static transformCodeSelection(documentFragment, target) {
const lineEls = documentFragment.querySelectorAll('.line'); let lineSelector = '.line';
let codeEl; if (target) {
if (lineEls.length > 1) { const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
codeEl = document.createElement('pre'); if (lineClass) {
codeEl.className = 'code highlight'; lineSelector = `.line_content.${lineClass} ${lineSelector}`;
}
}
const lineElements = documentFragment.querySelectorAll(lineSelector);
let codeElement;
if (lineElements.length > 1) {
codeElement = document.createElement('pre');
codeElement.className = 'code highlight';
const lang = lineEls[0].getAttribute('lang'); const lang = lineElements[0].getAttribute('lang');
if (lang) { if (lang) {
codeEl.setAttribute('lang', lang); codeElement.setAttribute('lang', lang);
} }
} else { } else {
codeEl = document.createElement('code'); codeElement = document.createElement('code');
} }
if (lineEls.length > 0) { if (lineElements.length > 0) {
for (let i = 0; i < lineEls.length; i += 1) { for (let i = 0; i < lineElements.length; i += 1) {
const lineEl = lineEls[i]; const lineElement = lineElements[i];
codeEl.appendChild(lineEl); codeElement.appendChild(lineElement);
codeEl.appendChild(document.createTextNode('\n')); codeElement.appendChild(document.createTextNode('\n'));
} }
} else { } else {
codeEl.appendChild(documentFragment); codeElement.appendChild(documentFragment);
} }
return codeEl; return codeElement;
} }
static nodeToGFM(node, respectWhitespaceParam = false) { static nodeToGFM(node, respectWhitespaceParam = false) {
......
...@@ -24,7 +24,8 @@ class Diff { ...@@ -24,7 +24,8 @@ class Diff {
if (!isBound) { if (!isBound) {
$(document) $(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this))
.on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this));
isBound = true; isBound = true;
} }
...@@ -100,6 +101,18 @@ class Diff { ...@@ -100,6 +101,18 @@ class Diff {
this.highlightSelectedLine(); this.highlightSelectedLine();
} }
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
table.removeClass('left-side-selected right-side-selected');
const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0];
if (lineClass) {
table.addClass(`${lineClass}-selected`);
}
}
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
} }
......
import Jed from 'jed'; import Jed from 'jed';
import sprintf from './sprintf';
/** /**
This is required to require all the translation folders in the current directory This is required to require all the translation folders in the current directory
this saves us having to do this manually & keep up to date with new languages this saves us having to do this manually & keep up to date with new languages
...@@ -66,4 +68,5 @@ export { lang }; ...@@ -66,4 +68,5 @@ export { lang };
export { gettext as __ }; export { gettext as __ };
export { ngettext as n__ }; export { ngettext as n__ };
export { pgettext as s__ }; export { pgettext as s__ };
export { sprintf };
export default locale; export default locale;
import _ from 'underscore';
/**
Very limited implementation of sprintf supporting only named parameters.
@param input (translated) text with parameters (e.g. '%{num_users} users use us')
@param parameters object mapping parameter names to values (e.g. { num_users: 5 })
@param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
@returns {String} the text with parameters replaces (e.g. '5 users use us')
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
**/
export default (input, parameters, escapeParameters = true) => {
let output = input;
if (parameters) {
Object.keys(parameters).forEach((parameterName) => {
const parameterValue = parameters[parameterName];
const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue;
output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue);
});
}
return output;
}
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
<button <button
v-if="showDisabledButton" v-if="showDisabledButton"
type="button" type="button"
class="btn btn-success btn-sm" class="js-disabled-merge-button btn btn-success btn-sm"
disabled="true"> disabled="true">
Merge Merge
</button> </button>
......
...@@ -16,9 +16,9 @@ export default { ...@@ -16,9 +16,9 @@ export default {
<div class="media-body"> <div class="media-body">
<mr-widget-author-and-time <mr-widget-author-and-time
actionText="Closed by" actionText="Closed by"
:author="mr.closedBy" :author="mr.closedEvent.author"
:dateTitle="mr.updatedAt" :dateTitle="mr.closedEvent.updatedAt"
:dateReadable="mr.closedAt" :dateReadable="mr.closedEvent.formattedUpdatedAt"
/> />
<section class="mr-info-list"> <section class="mr-info-list">
<p> <p>
......
...@@ -10,8 +10,17 @@ export default { ...@@ -10,8 +10,17 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon
status="failed"
showDisabledButton />
<div class="media-body space-children"> <div class="media-body space-children">
<span
v-if="mr.shouldBeRebased"
class="bold">
Fast-forward merge is not possible.
To merge this request, first rebase locally.
</span>
<template v-else>
<span class="bold"> <span class="bold">
There are merge conflicts<span v-if="!mr.canMerge">.</span> There are merge conflicts<span v-if="!mr.canMerge">.</span>
<span v-if="!mr.canMerge"> <span v-if="!mr.canMerge">
...@@ -21,16 +30,17 @@ export default { ...@@ -21,16 +30,17 @@ export default {
<a <a
v-if="mr.canMerge && mr.conflictResolutionPath" v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath" :href="mr.conflictResolutionPath"
class="btn btn-default btn-xs js-resolve-conflicts-button"> class="js-resolve-conflicts-button btn btn-default btn-xs">
Resolve conflicts Resolve conflicts
</a> </a>
<a <a
v-if="mr.canMerge" v-if="mr.canMerge"
class="btn btn-default btn-xs js-merge-locally-button" class="js-merge-locally-button btn btn-default btn-xs"
data-toggle="modal" data-toggle="modal"
href="#modal_merge_info"> href="#modal_merge_info">
Merge locally Merge locally
</a> </a>
</template>
</div> </div>
</div> </div>
`, `,
......
...@@ -69,9 +69,9 @@ export default { ...@@ -69,9 +69,9 @@ export default {
<div class="space-children"> <div class="space-children">
<mr-widget-author-and-time <mr-widget-author-and-time
actionText="Merged by" actionText="Merged by"
:author="mr.mergedBy" :author="mr.mergedEvent.author"
:dateTitle="mr.updatedAt" :date-title="mr.mergedEvent.updatedAt"
:dateReadable="mr.mergedAt" /> :date-readable="mr.mergedEvent.formattedUpdatedAt" />
<a <a
v-if="mr.canRevertInCurrentMR" v-if="mr.canRevertInCurrentMR"
v-tooltip v-tooltip
......
...@@ -284,10 +284,16 @@ export default { ...@@ -284,10 +284,16 @@ export default {
:mr="mr" :mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" /> :is-merge-button-disabled="isMergeButtonDisabled" />
<span
v-if="mr.ffOnlyEnabled"
class="js-fast-forward-message">
Fast-forward merge without a merge commit
</span>
<button <button
v-else
@click="toggleCommitMessageEditor" @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
class="btn btn-default btn-xs" class="js-modify-commit-message-button btn btn-default btn-xs"
type="button"> type="button">
Modify commit message Modify commit message
</button> </button>
......
...@@ -37,10 +37,8 @@ export default class MergeRequestStore { ...@@ -37,10 +37,8 @@ export default class MergeRequestStore {
} }
this.updatedAt = data.updated_at; this.updatedAt = data.updated_at;
this.mergedAt = MergeRequestStore.getEventDate(data.merge_event); this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
this.closedAt = MergeRequestStore.getEventDate(data.closed_event); this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} }); this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.mergeUserId = data.merge_user_id; this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id; this.currentUserId = gon.current_user_id;
...@@ -57,6 +55,8 @@ export default class MergeRequestStore { ...@@ -57,6 +55,8 @@ export default class MergeRequestStore {
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path; this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = !!data.should_be_rebased;
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;
...@@ -118,6 +118,14 @@ export default class MergeRequestStore { ...@@ -118,6 +118,14 @@ export default class MergeRequestStore {
} }
} }
static getEventObject(event) {
return {
author: MergeRequestStore.getAuthorObject(event),
updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
formattedUpdatedAt: MergeRequestStore.getEventDate(event),
};
}
static getAuthorObject(event) { static getAuthorObject(event) {
if (!event) { if (!event) {
return {}; return {};
...@@ -131,6 +139,14 @@ export default class MergeRequestStore { ...@@ -131,6 +139,14 @@ export default class MergeRequestStore {
}; };
} }
static getEventUpdatedAtDate(event) {
if (!event) {
return '';
}
return event.updated_at;
}
static getEventDate(event) { static getEventDate(event) {
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
...@@ -138,7 +154,7 @@ export default class MergeRequestStore { ...@@ -138,7 +154,7 @@ export default class MergeRequestStore {
return ''; return '';
} }
return timeagoInstance.format(event.updated_at); return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
} }
} }
...@@ -2,6 +2,7 @@ import { ...@@ -2,6 +2,7 @@ import {
__, __,
n__, n__,
s__, s__,
sprintf,
} from '../locale'; } from '../locale';
export default (Vue) => { export default (Vue) => {
...@@ -37,6 +38,7 @@ export default (Vue) => { ...@@ -37,6 +38,7 @@ export default (Vue) => {
@returns {String} Translated context based text @returns {String} Translated context based text
**/ **/
s__, s__,
sprintf,
}, },
}); });
}; };
...@@ -873,6 +873,13 @@ ...@@ -873,6 +873,13 @@
min-width: 100%; min-width: 100%;
} }
} }
header.navbar-gitlab-new .header-content .dropdown {
.dropdown-menu {
left: 0;
min-width: 100%;
}
}
} }
@include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.breadcrumbs-list .dropdown ');
......
...@@ -229,6 +229,10 @@ ul.content-list { ...@@ -229,6 +229,10 @@ ul.content-list {
.label-default { .label-default {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.avatar-cell {
align-self: flex-start;
}
} }
.panel > .content-list > li { .panel > .content-list > li {
......
...@@ -77,6 +77,18 @@ ...@@ -77,6 +77,18 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
&.left-side-selected {
td.line_content.parallel.right-side {
@include user-select(none);
}
}
&.right-side-selected {
td.line_content.parallel.left-side {
@include user-select(none);
}
}
} }
tr.line_holder.parallel { tr.line_holder.parallel {
......
...@@ -15,9 +15,9 @@ module NotesActions ...@@ -15,9 +15,9 @@ module NotesActions
notes = notes_finder.execute notes = notes_finder.execute
.inc_relations_for_view .inc_relations_for_view
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes_json[:notes] = notes_json[:notes] =
if noteable.discussions_rendered_on_frontend? if noteable.discussions_rendered_on_frontend?
......
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update, :move] before_action :authorize_update_issue!, only: [:update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -63,10 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -63,10 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end end
def edit
respond_with(@issue)
end
def show def show
@noteable = @issue @noteable = @issue
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
...@@ -126,10 +122,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -126,10 +122,6 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format| respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do format.json do
render_issue_json render_issue_json
end end
......
...@@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController
response.headers['Content-Security-Policy'] = "default-src 'none'" response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'" response.headers['X-Content-Security-Policy'] = "default-src 'none'"
if file.on_disk?
send_file file.on_disk_path, disposition: 'inline'
else
send_data( send_data(
file.raw_data, file.raw_data,
type: file.mime_type, type: file.mime_type,
disposition: 'inline', disposition: 'inline',
filename: file.name filename: file.name
) )
end
else else
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki) @page = WikiPage.new(@project_wiki)
......
...@@ -344,6 +344,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -344,6 +344,7 @@ class ProjectsController < Projects::ApplicationController
:tag_list, :tag_list,
:visibility_level, :visibility_level,
:template_name, :template_name,
:merge_method,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
......
...@@ -33,19 +33,21 @@ module DiffHelper ...@@ -33,19 +33,21 @@ module DiffHelper
end end
def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" content_line_class = %w[line_content match]
cls = ['diff-line-num', 'unfold', 'js-unfold'] content_line_class << 'parallel' if view == :parallel
cls << 'js-unfold-bottom' if bottom
line_num_class = %w[diff-line-num unfold js-unfold]
line_num_class << 'js-unfold-bottom' if bottom
html = '' html = ''
if old_pos if old_pos
html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content unless view == :inline html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
end end
if new_pos if new_pos
html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
html << content html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end end
html.html_safe html.html_safe
......
...@@ -524,6 +524,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -524,6 +524,14 @@ class MergeRequest < ActiveRecord::Base
true true
end end
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
def should_be_rebased?
project.ff_merge_must_be_possible? && !ff_merge_possible?
end
def can_cancel_merge_when_pipeline_succeeds?(current_user) def can_cancel_merge_when_pipeline_succeeds?(current_user)
can_be_merged_by?(current_user) || self.author == current_user can_be_merged_by?(current_user) || self.author == current_user
end end
......
...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base ...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
# Storage specific hooks # Storage specific hooks
after_initialize :use_hashed_storage after_initialize :use_hashed_storage
after_create :check_repository_absence!
after_create :ensure_storage_path_exists after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed? after_save :ensure_storage_path_exists, if: :namespace_id_changed?
...@@ -229,7 +230,7 @@ class Project < ActiveRecord::Base ...@@ -229,7 +230,7 @@ class Project < ActiveRecord::Base
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create validate :check_limit, on: :create
validate :check_repository_path_availability, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? } validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
validate :avatar_type, validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? } if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
...@@ -1026,7 +1027,9 @@ class Project < ActiveRecord::Base ...@@ -1026,7 +1027,9 @@ class Project < ActiveRecord::Base
expires_full_path_cache # we need to clear cache to validate renames correctly expires_full_path_cache # we need to clear cache to validate renames correctly
if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") # Check if repository with same path already exists on disk we can
# skip this for the hashed storage because the path does not change
if legacy_storage? && repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk') errors.add(:base, 'There is already a repository with that name on disk')
return false return false
end end
...@@ -1567,6 +1570,34 @@ class Project < ActiveRecord::Base ...@@ -1567,6 +1570,34 @@ class Project < ActiveRecord::Base
persisted? && path_changed? persisted? && path_changed?
end end
def merge_method
if self.merge_requests_ff_only_enabled
:ff
elsif self.merge_requests_rebase_enabled
:rebase_merge
else
:merge
end
end
def merge_method=(method)
case method.to_s
when "ff"
self.merge_requests_ff_only_enabled = true
self.merge_requests_rebase_enabled = true
when "rebase_merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = true
when "merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = false
end
end
def ff_merge_must_be_possible?
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
def migrate_to_hashed_storage! def migrate_to_hashed_storage!
return if hashed_storage? return if hashed_storage?
...@@ -1614,6 +1645,19 @@ class Project < ActiveRecord::Base ...@@ -1614,6 +1645,19 @@ class Project < ActiveRecord::Base
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
end end
def check_repository_absence!
return if skip_disk_validation
if repository_storage_path.blank? || repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
throw :abort
end
end
def repository_with_same_path_already_exists?
gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
end
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
def set_last_activity_at def set_last_activity_at
update_column(:last_activity_at, self.created_at) update_column(:last_activity_at, self.created_at)
......
...@@ -54,12 +54,15 @@ class ProjectWiki ...@@ -54,12 +54,15 @@ class ProjectWiki
[Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('') [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('')
end end
# Returns the Gollum::Wiki object. # Returns the Gitlab::Git::Wiki object.
def wiki def wiki
@wiki ||= begin @wiki ||= begin
Gollum::Wiki.new(path_to_repo) gl_repository = Gitlab::GlRepository.gl_repository(project, true)
rescue Rugged::OSError raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository)
create_repo!
create_repo!(raw_repository) unless raw_repository.exists?
Gitlab::Git::Wiki.new(raw_repository)
end end
end end
...@@ -86,20 +89,14 @@ class ProjectWiki ...@@ -86,20 +89,14 @@ class ProjectWiki
# Returns an initialized WikiPage instance or nil # Returns an initialized WikiPage instance or nil
def find_page(title, version = nil) def find_page(title, version = nil)
page_title, page_dir = page_title_and_dir(title) page_title, page_dir = page_title_and_dir(title)
if page = wiki.page(page_title, version, page_dir)
if page = wiki.page(title: page_title, version: version, dir: page_dir)
WikiPage.new(self, page, true) WikiPage.new(self, page, true)
else
nil
end end
end end
def find_file(name, version = nil, try_on_disk = true) def find_file(name, version = nil)
version = wiki.ref if version.nil? # Gollum::Wiki#file ? wiki.file(name, version)
if wiki_file = wiki.file(name, version, try_on_disk)
wiki_file
else
nil
end
end end
def create_page(title, content, format = :markdown, message = nil) def create_page(title, content, format = :markdown, message = nil)
...@@ -108,7 +105,7 @@ class ProjectWiki ...@@ -108,7 +105,7 @@ class ProjectWiki
wiki.write_page(title, format.to_sym, content, commit) wiki.write_page(title, format.to_sym, content, commit)
update_project_activity update_project_activity
rescue Gollum::DuplicatePageError => e rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}" @error_message = "Duplicate page: #{e.message}"
return false return false
end end
...@@ -116,13 +113,13 @@ class ProjectWiki ...@@ -116,13 +113,13 @@ class ProjectWiki
def update_page(page, content:, title: nil, format: :markdown, message: nil) def update_page(page, content:, title: nil, format: :markdown, message: nil)
commit = commit_details(:updated, message, page.title) commit = commit_details(:updated, message, page.title)
wiki.update_page(page, title || page.name, format.to_sym, content, commit) wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
update_project_activity update_project_activity
end end
def delete_page(page, message = nil) def delete_page(page, message = nil)
wiki.delete_page(page, commit_details(:deleted, message, page.title)) wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_project_activity update_project_activity
end end
...@@ -145,20 +142,8 @@ class ProjectWiki ...@@ -145,20 +142,8 @@ class ProjectWiki
wiki.class.default_ref wiki.class.default_ref
end end
def create_repo!
if init_repo(disk_path)
wiki = Gollum::Wiki.new(path_to_repo)
else
raise CouldNotCreateWikiError
end
repository.after_create
wiki
end
def ensure_repository def ensure_repository
create_repo! unless repository_exists? raise CouldNotCreateWikiError unless wiki.repository_exists?
end end
def hook_attrs def hook_attrs
...@@ -173,24 +158,24 @@ class ProjectWiki ...@@ -173,24 +158,24 @@ class ProjectWiki
private private
def init_repo(disk_path) def create_repo!(raw_repository)
gitlab_shell.add_repository(project.repository_storage, disk_path) gitlab_shell.add_repository(project.repository_storage, disk_path)
raise CouldNotCreateWikiError unless raw_repository.exists?
repository.after_create
end end
def commit_details(action, message = nil, title = nil) def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title) commit_message = message || default_message(action, title)
{ email: @user.email, name: @user.name, message: commit_message } Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
end end
def default_message(action, title) def default_message(action, title)
"#{@user.username} #{action} page: #{title}" "#{@user.username} #{action} page: #{title}"
end end
def path_to_repo
@path_to_repo ||= File.join(project.repository_storage_path, "#{disk_path}.git")
end
def update_project_activity def update_project_activity
@project.touch(:last_activity_at, :last_repository_updated_at) @project.touch(:last_activity_at, :last_repository_updated_at)
end end
......
...@@ -850,6 +850,25 @@ class Repository ...@@ -850,6 +850,25 @@ class Repository
end end
end end
def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target
their_commit =
if source.is_a?(Gitlab::Git::Commit)
source.raw_commit
else
rugged.lookup(source)
end
raise 'Invalid merge target' if our_commit.nil?
raise 'Invalid merge source' if their_commit.nil?
with_branch(user, target_branch) do |start_commit|
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
their_commit.oid
end
end
def revert( def revert(
user, commit, branch_name, message, user, commit, branch_name, message,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
......
...@@ -50,7 +50,7 @@ class WikiPage ...@@ -50,7 +50,7 @@ class WikiPage
# The Gitlab ProjectWiki instance. # The Gitlab ProjectWiki instance.
attr_reader :wiki attr_reader :wiki
# The raw Gollum::Page instance. # The raw Gitlab::Git::WikiPage instance.
attr_reader :page attr_reader :page
# The attributes Hash used for storing and validating # The attributes Hash used for storing and validating
...@@ -75,7 +75,7 @@ class WikiPage ...@@ -75,7 +75,7 @@ class WikiPage
if @attributes[:slug].present? if @attributes[:slug].present?
@attributes[:slug] @attributes[:slug]
else else
wiki.wiki.preview_page(title, '', format).url_path wiki.wiki.preview_slug(title, format)
end end
end end
...@@ -131,7 +131,7 @@ class WikiPage ...@@ -131,7 +131,7 @@ class WikiPage
def versions def versions
return [] unless persisted? return [] unless persisted?
@page.versions wiki.wiki.page_versions(@page.path)
end end
def commit def commit
...@@ -264,8 +264,8 @@ class WikiPage ...@@ -264,8 +264,8 @@ class WikiPage
end end
page_title, page_dir = wiki.page_title_and_dir(page_details) page_title, page_dir = wiki.page_title_and_dir(page_details)
gollum_wiki = wiki.wiki gitlab_git_wiki = wiki.wiki
@page = gollum_wiki.paged(page_title, page_dir) @page = gitlab_git_wiki.page(title: page_title, dir: page_dir)
set_attributes set_attributes
@persisted = errors.blank? @persisted = errors.blank?
......
...@@ -13,6 +13,11 @@ class MergeRequestEntity < IssuableEntity ...@@ -13,6 +13,11 @@ class MergeRequestEntity < IssuableEntity
expose :target_branch expose :target_branch
expose :target_project_id expose :target_project_id
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
merge_request.project.merge_requests_ff_only_enabled
end
# Events # Events
expose :merge_event, using: EventEntity expose :merge_event, using: EventEntity
expose :closed_event, using: EventEntity expose :closed_event, using: EventEntity
......
module MergeRequests
# MergeService class
#
# Do git fast-forward merge and in case of success
# mark merge request as merged and execute all hooks and notifications
# Executed when you do fast-forward merge via GitLab UI
#
class FfMergeService < MergeRequests::MergeService
private
def commit
repository.ff_merge(current_user,
source,
merge_request.target_branch,
merge_request: merge_request)
rescue Gitlab::Git::HooksService::PreReceiveError => e
raise MergeError, e.message
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
end
end
end
...@@ -11,6 +11,11 @@ module MergeRequests ...@@ -11,6 +11,11 @@ module MergeRequests
attr_reader :merge_request, :source attr_reader :merge_request, :source
def execute(merge_request) def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
return
end
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? unless @merge_request.mergeable?
......
- form = local_assigns.fetch(:form)
- project = local_assigns.fetch(:project)
.radio
= label_tag :project_merge_method_ff do
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio"
%strong Fast-forward merge
%br
%span.descr
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
%br
%span.descr
When fast-forward merge is not possible, the user must first rebase locally.
- form = local_assigns.fetch(:form)
.radio
= label_tag :project_merge_method_rebase_merge do
= form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio"
%strong Merge commit with semi-linear history
%br
%span.descr
A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
%br
%span.descr
When fast-forward merge is not possible, the user must first rebase locally.
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
.form-group
= label_tag :merge_method_merge, class: 'label-light' do
Merge method
.radio
= label_tag :project_merge_method_merge do
= form.radio_button :merge_method, :merge, class: "js-merge-method-radio"
%strong Merge commit
%br
%span.descr
A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
= render 'merge_request_rebase_settings', form: form
= render 'merge_request_fast_forward_settings', project: @project, form: form
= render 'projects/merge_request_merge_settings', form: form = render 'projects/merge_request_merge_settings', form: form
...@@ -5,25 +5,24 @@ ...@@ -5,25 +5,24 @@
= diff_match_line @form.since, @form.since, text: @match_line, view: diff_view = diff_match_line @form.since, @form.since, text: @match_line, view: diff_view
- @lines.each_with_index do |line, index| - @lines.each_with_index do |line, index|
- line_new = index + @form.since - line_number_new = index + @form.since
- line_old = line_new - @form.offset - line_number_old = line_number_new - @form.offset
- line_content = capture do - line[0, 0] = ' ' * @form.indent
%td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line} %tr.line_holder.diff-expanded{ id: line_number_old, class: line_class }
%tr.line_holder.diff-expanded{ id: line_old, class: line_class }
- case diff_view - case diff_view
- when :inline - when :inline
%td.old_line.diff-line-num{ data: { linenumber: line_old } } %td.old_line.diff-line-num{ data: { linenumber: line_number_old } }
%a{ href: "#", data: { linenumber: line_old }, disabled: true } %a{ href: "#", data: { linenumber: line_number_old }, disabled: true }
%td.new_line.diff-line-num{ data: { linenumber: line_new } } %td.new_line.diff-line-num{ data: { linenumber: line_number_new } }
%a{ href: "#", data: { linenumber: line_new }, disabled: true } %a{ href: "#", data: { linenumber: line_number_new }, disabled: true }
= line_content %td.line_content.noteable_line{ class: line_class }= line
- when :parallel - when :parallel
%td.old_line.diff-line-num{ data: { linenumber: line_old } } %td.old_line.diff-line-num{ data: { linenumber: line_number_old } }
%a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true } %a{ href: "##{line_number_old}", data: { linenumber: line_number_old }, disabled: true }
= line_content %td.line_content.noteable_line.left-side{ class: line_class }= line
%td.new_line.diff-line-num{ data: { linenumber: line_new } } %td.new_line.diff-line-num{ data: { linenumber: line_number_new } }
%a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true } %a{ href: "##{line_number_new}", data: { linenumber: line_number_new }, disabled: true }
= line_content %td.line_content.noteable_line.right-side{ class: line_class }= line
- if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
%tr.line_holder{ id: @form.to, class: line_class } %tr.line_holder{ id: @form.to, class: line_class }
......
...@@ -14,20 +14,20 @@ ...@@ -14,20 +14,20 @@
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel = diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- when 'old-nonewline', 'new-nonewline' - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num %td.old_line.diff-line-num
%td.line_content.match= left.text %td.line_content.match.left-side= left.text
- else - else
- left_line_code = diff_file.line_code(left) - left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left) - left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old') = add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first) - discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable? - if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id } %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text) %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel.left-side
- if right - if right
- case right.type - case right.type
...@@ -35,20 +35,20 @@ ...@@ -35,20 +35,20 @@
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel = diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- when 'old-nonewline', 'new-nonewline' - when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.match= right.text %td.line_content.match.right-side= right.text
- else - else
- right_line_code = diff_file.line_code(right) - right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right) - right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new') = add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first) - discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable? - if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id } %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text) %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel.right-side
- if discussions_left || discussions_right - if discussions_left || discussions_right
= render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right = render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right
......
- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
%hr
= render "form"
...@@ -29,13 +29,13 @@ ...@@ -29,13 +29,13 @@
commit.id, index == 0) do commit.id, index == 0) do
= truncate_sha(commit.id) = truncate_sha(commit.id)
%td %td
= commit.author.name = commit.author_name
%td %td
= commit.message = commit.message
%td %td
#{time_ago_with_tooltip(version.authored_date)} #{time_ago_with_tooltip(version.authored_date)}
%td %td
%strong %strong
= @page.page.wiki.page(@page.page.name, commit.id).try(:format) = version.format
= render 'sidebar' = render 'sidebar'
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.nav-text .nav-text
%h2.wiki-page-title= @page.title.capitalize %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by %span.wiki-last-edit-by
= (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author.name}</strong>" }).html_safe = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.commit.authored_date)} #{time_ago_with_tooltip(@page.commit.authored_date)}
.nav-controls .nav-controls
......
---
title: Remove the ability to visit the issue edit form directly
merge_request: 14523
author:
type: removed
---
title: Does not check if an invariant hashed storage path exists on disk when renaming
projects.
merge_request: 14428
author:
type: fixed
---
title: Fix navigation dropdown close animation on mobile screens
merge_request: 14649
author:
type: fixed
---
title: Whitelist authorized_keys.lock in the gitlab:check rake task
merge_request: 14624
author:
type: fixed
---
title: Add (partial) index on Labels.template
merge_request:
author:
type: other
---
title: "Add \"implements\" to the default issue closing message regex"
merge_request: 14612
author: Guilherme Vieira
type: added
---
title: Fixed commit avatars being centered vertically
merge_request:
author:
type: fixed
---
title: Only copy old/new code when selecting left/right side of parallel diff
merge_request:
author:
type: added
---
title: Add documentation to summarise project archiving
merge_request: 14650
author:
type: other
---
title: Move Custom merge methods from EE
merge_request:
author:
type: added
---
title: 'Kubernetes integration: ensure v1.8.0 compatibility'
merge_request: 14635
author:
type: fixed
---
title: Bump google-api-client Gem from 0.8.6 to 0.13.6
merge_request:
author:
type: other
---
title: Fixed merge request widget merged & closed date tooltip text
merge_request:
author:
type: fixed
---
title: Update GitLab Pages to v0.6.0
merge_request: 14630
author:
type: other
---
title: Add basic sprintf implementation to JavaScript
merge_request: 14506
author:
type: other
...@@ -16,7 +16,7 @@ Rails.application.configure do ...@@ -16,7 +16,7 @@ Rails.application.configure do
config.cache_classes = ENV['CACHE_CLASSES'] == 'true' config.cache_classes = ENV['CACHE_CLASSES'] == 'true'
# Configure static asset server for tests with Cache-Control for performance # Configure static asset server for tests with Cache-Control for performance
config.assets.digest = false config.assets.compile = false if ENV['CI']
config.serve_static_files = true config.serve_static_files = true
config.static_cache_control = "public, max-age=3600" config.static_cache_control = "public, max-age=3600"
......
...@@ -89,7 +89,7 @@ production: &base ...@@ -89,7 +89,7 @@ production: &base
# This happens when the commit is pushed or merged into the default branch of a project. # This happens when the commit is pushed or merged into the default branch of a project.
# When not specified the default issue_closing_pattern as specified below will be used. # When not specified the default issue_closing_pattern as specified below will be used.
# Tip: you can test your closing pattern at http://rubular.com. # Tip: you can test your closing pattern at http://rubular.com.
# issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)'
## Default project features settings ## Default project features settings
default_projects_features: default_projects_features:
......
...@@ -257,7 +257,7 @@ Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled']. ...@@ -257,7 +257,7 @@ Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].
Settings.gitlab['password_authentication_enabled'] ||= true if Settings.gitlab['password_authentication_enabled'].nil? Settings.gitlab['password_authentication_enabled'] ||= true if Settings.gitlab['password_authentication_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10
......
require 'logger'
GRPC_LOGGER = Logger.new(Rails.root.join('log/grpc.log'))
GRPC_LOGGER.level = ENV['GRPC_LOG_LEVEL'].presence || 'WARN'
GRPC_LOGGER.progname = 'GRPC'
module GRPC
def self.logger
GRPC_LOGGER
end
end
# rubocop:disable all
class AddMergeRequestRebaseEnabledToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:projects, :merge_requests_rebase_enabled, :boolean, default: false)
end
def down
remove_column(:projects, :merge_requests_rebase_enabled)
end
end
# rubocop:disable all
class AddFastForwardOptionToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def add
add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
end
def down
remove_column(:projects, :merge_requests_ff_only_enabled)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPartialIndexForLabelsTemplate < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
# Note this is a partial index in Postgres but MySQL will ignore the
# partial index clause. By making it an index on "template" this
# means the index will still accomplish the same goal of optimizing
# a query with "where template = true" on MySQL -- it'll just take
# more space. In this case the number of records with template=true
# is expected to be very small (small enough to display on a single
# web page) so it's ok to filter or sort them without the index
# anyways.
def up
add_concurrent_index "labels", ["template"], where: "template"
end
def down
remove_concurrent_index "labels", ["template"], where: "template"
end
end
...@@ -759,6 +759,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do ...@@ -759,6 +759,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
add_index "labels", ["template"], name: "index_labels_on_template", where: "template", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
...@@ -1246,6 +1247,8 @@ ActiveRecord::Schema.define(version: 20170928100231) do ...@@ -1246,6 +1247,8 @@ ActiveRecord::Schema.define(version: 20170928100231) do
t.integer "storage_version", limit: 2 t.integer "storage_version", limit: 2
t.boolean "resolve_outdated_diff_discussions" t.boolean "resolve_outdated_diff_discussions"
t.boolean "repository_read_only" t.boolean "repository_read_only"
t.boolean "merge_requests_ff_only_enabled", default: false
t.boolean "merge_requests_rebase_enabled", default: false, null: false
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -32,6 +32,14 @@ prometheus_listen_addr = "localhost:9236" ...@@ -32,6 +32,14 @@ prometheus_listen_addr = "localhost:9236"
Changes to `/home/git/gitaly/config.toml` are applied when you run `service Changes to `/home/git/gitaly/config.toml` are applied when you run `service
gitlab restart`. gitlab restart`.
## Client-side GRPC logs
Gitaly uses the [gRPC](https://grpc.io/) RPC framework. The Ruby gRPC
client has its own log file which may contain useful information when
you are seeing Gitaly errors. You can control the log level of the
gRPC client with the `GRPC_LOG_LEVEL` environment variable. The
default level is `WARN`.
## Running Gitaly on its own server ## Running Gitaly on its own server
> This is an optional way to deploy Gitaly which can benefit GitLab > This is an optional way to deploy Gitaly which can benefit GitLab
......
...@@ -183,13 +183,20 @@ aren't in the message with id `1 pipeline`. ...@@ -183,13 +183,20 @@ aren't in the message with id `1 pipeline`.
### Interpolation ### Interpolation
- In Ruby/HAML: - In Ruby/HAML (see [sprintf]):
```ruby ```ruby
_("Hello %{name}") % { name: 'Joe' } _("Hello %{name}") % { name: 'Joe' }
``` ```
- In JavaScript: Not supported at this moment. - In JavaScript: Only named parameters are supported (see also [#37992]):
```javascript
__("Hello %{name}") % { name: 'Joe' }
```
[sprintf]: http://ruby-doc.org/core/Kernel.html#method-i-sprintf
[#37992]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
### Plurals ### Plurals
......
...@@ -39,6 +39,12 @@ When information is updating in place, a quick, subtle animation is needed. The ...@@ -39,6 +39,12 @@ When information is updating in place, a quick, subtle animation is needed. The
![Quick update animation](img/animation-quickupdate.gif) ![Quick update animation](img/animation-quickupdate.gif)
### Skeleton loading
Skeleton loading is explained in the [component section](components.html#skeleton-loading) of the UX guide. It includes a horizontally pulsating animation that shows motion as if it's growing. It's timing is a slower `linear 1s`.
![Skeleton loading animation](img/skeleton-loading.gif)
### Moving transitions ### Moving transitions
When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`. When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`.
...@@ -51,7 +57,9 @@ View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here. ...@@ -51,7 +57,9 @@ View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here.
![Reorder animation](img/animation-reorder.gif) ![Reorder animation](img/animation-reorder.gif)
#### Autoscroll the page #### Autoscroll the page
Another example of a moving transition is when you have to autoscroll the page to keep an active element visible. Another example of a moving transition is when you have to autoscroll the page to keep an active element visible.
View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here. View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here.
![Autoscroll animation](img/animation-autoscroll.gif) ![Autoscroll animation](img/animation-autoscroll.gif)
...@@ -204,6 +204,25 @@ Cover blocks are generally used to create a heading element for a page, such as ...@@ -204,6 +204,25 @@ Cover blocks are generally used to create a heading element for a page, such as
--- ---
## Skeleton loading
Skeleton loading is a way to convey to the user what kind of content is currently being loaded. It's a paradigm with which content can independently and asynchronously be loaded, while still adhering to the structure and look of the completely loaded view.
### Requirements
* A skeleton should represent an organism in a recognisable way
* Atom elements within organisms (for reference see this article on [atomic design methodology](http://atomicdesign.bradfrost.com/chapter-2/)) may be represented in a maximum of 3 repetitions, if applicable.
* Skeletons should only be presented in grayscale using the HEX colors: `#fafafa` or `#ffffff` (except for shadows)
* Animate the grey atoms in a pulsating way to show motion, as if "loading". The pulse animation transitions colors horizontally from left to right, starting with `#f2f2f2` to `#fafafa`.
![Skeleton loading animation](img/skeleton-loading.gif)
### Usage
Skeleton loading can replace any existing UI elements for the period in which they are loaded and should aim for maintaining a similar structure visually.
---
## Panels ## Panels
> TODO: Catalog how we are currently using panels and rationalize how they relate to alerts > TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
......
# Fast-forward merge requests
Retain a linear Git history and a way to accept merge requests without
creating merge commits.
## Overview
When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge
commits will be created and all merges are fast-forwarded, which means that
merging is only allowed if the branch could be fast-forwarded.
When a fast-forward merge is not possible, the user must rebase the branch manually.
## Use cases
Sometimes, a workflow policy might mandate a clean commit history without
merge commits. In such cases, the fast-forward merge is the perfect candidate.
## Enabling fast-forward merges
1. Navigate to your project's **Settings** and search for the 'Merge method'
1. Select the **Fast-forward merge** option
1. Hit **Save changes** for the changes to take effect
Now, when you visit the merge request page, you will be able to accept it
**only if a fast-forward merge is possible**.
![Fast forward merge request](img/ff_merge_mr.png)
If the target branch is ahead of the source branch, you need to rebase the
source branch locally before you will be able to do a fast-forward merge.
![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png)
[ffonly]: https://git-scm.com/docs/git-merge#git-merge---ff-only
...@@ -23,12 +23,14 @@ With GitLab merge requests, you can: ...@@ -23,12 +23,14 @@ With GitLab merge requests, you can:
- Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md) - Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md)
- Add a time estimation and the time spent with that merge request with [Time Tracking](../../../workflow/time_tracking.html#time-tracking) - Add a time estimation and the time spent with that merge request with [Time Tracking](../../../workflow/time_tracking.html#time-tracking)
- [Resolve merge conflicts from the UI](#resolve-conflicts) - [Resolve merge conflicts from the UI](#resolve-conflicts)
- Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
With **[GitLab Enterprise Edition][ee]**, you can also: With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium) - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter) - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter)
- Enable [fast-forward merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/fast_forward_merge.html) (available in GitLab Enterprise Edition Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter) - [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter)
- Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter) - Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter)
- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter) - Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter)
...@@ -89,6 +91,22 @@ in a merged merge requests or a commit. ...@@ -89,6 +91,22 @@ in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](cherry_pick_changes.md) [Learn more about cherry-picking changes.](cherry_pick_changes.md)
## Semi-linear history merge requests
A merge commit is created for every merge, but the branch is only merged if
a fast-forward merge is possible. This ensures that if the merge request build
succeeded, the target branch build will also succeed after merging.
Navigate to a project's settings, select the **Merge commit with semi-linear
history** option under **Merge Requests: Merge method** and save your changes.
## Fast-forward merge requests
If you prefer a linear Git history and a way to accept merge requests without
creating merge commits, you can configure this on a per-project basis.
[Read more about fast-forward merge requests.](fast_forward_merge.md)
## Merge when pipeline succeeds ## Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or When reviewing a merge request that looks ready to merge but still has one or
......
...@@ -23,7 +23,7 @@ Add an [issue description template](../description_templates.md#description-temp ...@@ -23,7 +23,7 @@ Add an [issue description template](../description_templates.md#description-temp
Set up your project's merge request settings: Set up your project's merge request settings:
- Set up the merge request method (merge commit, [fast-forward merge](https://docs.gitlab.com/ee/user/project/merge_requests/fast_forward_merge.html#fast-forward-merge-requests)). _Fast-forward is available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)._ - Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)).
- Merge request [description templates](../description_templates.md#description-templates). - Merge request [description templates](../description_templates.md#description-templates).
- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)_. - Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)_.
- Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md). - Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
...@@ -42,3 +42,11 @@ Learn how to [export a project](import_export.md#importing-the-project) in GitLa ...@@ -42,3 +42,11 @@ Learn how to [export a project](import_export.md#importing-the-project) in GitLa
### Advanced settings ### Advanced settings
Here you can run housekeeping, archive, rename, transfer, or remove a project. Here you can run housekeeping, archive, rename, transfer, or remove a project.
#### Archiving a project
>**Note:** Only Project Owners and Admin users have the permission to archive a project
It's possible to mark a project as archived via the Project Settings. An archived project will be hidden by default in the project listings.
An archived project can be fully restored and will therefore retain it's repository and all associated resources whilst in an archived state.
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
- [Revert changes in the UI](../user/project/merge_requests/revert_changes.md) - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
- [Merge requests versions](../user/project/merge_requests/versions.md) - [Merge requests versions](../user/project/merge_requests/versions.md)
- ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md) - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
- [Fast-forward merge requests](../user/project/merge_requests/fast_forward_merge.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md) - [Todos](todos.md)
......
Feature: Project Ff Merge Requests
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" have "Bug NS-05" open merge request with diffs inside
And merge request "Bug NS-05" is mergeable
@javascript
Scenario: I do ff-only merge for rebased branch
Given ff merge enabled
And merge request "Bug NS-05" is rebased
When I visit merge request page "Bug NS-05"
Then I should see ff-only merge button
When I accept this merge request
Then I should see merged request
@javascript
Scenario: I do ff-only merge for merged branch
Given ff merge enabled
And merge request "Bug NS-05" merged target
When I visit merge request page "Bug NS-05"
Then I should see ff-only merge button
When I accept this merge request
Then I should see merged request
class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
include SharedAuthentication
include SharedIssuable
include SharedProject
include SharedNote
include SharedPaths
include SharedMarkdown
include SharedDiffNote
include SharedUser
include WaitForRequests
step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
create(:merge_request_with_diffs,
title: "Bug NS-05",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'I should see ff-only merge button' do
expect(page).to have_content "Fast-forward merge without a merge commit"
expect(page).to have_button 'Merge'
end
step 'merge request "Bug NS-05" is mergeable' do
merge_request.mark_as_mergeable
end
step 'I accept this merge request' do
page.within '.mr-state-widget' do
click_button "Merge"
end
end
step 'I should see merged request' do
page.within '.status-box' do
expect(page).to have_content "Merged"
wait_for_requests
end
end
step 'ff merge enabled' do
project = merge_request.target_project
project.merge_requests_ff_only_enabled = true
project.save!
end
step 'merge request "Bug NS-05" is rebased' do
merge_request.source_branch = 'flatten-dir'
merge_request.target_branch = 'improve/awesome'
merge_request.reload_diff
merge_request.save!
end
step 'merge request "Bug NS-05" merged target' do
merge_request.source_branch = 'merged-target'
merge_request.target_branch = 'improve/awesome'
merge_request.reload_diff
merge_request.save!
end
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
end
...@@ -232,7 +232,7 @@ module SharedDiffNote ...@@ -232,7 +232,7 @@ module SharedDiffNote
end end
def click_parallel_diff_line(code, line_type) def click_parallel_diff_line(code, line_type)
find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover'
find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
end end
end end
...@@ -73,8 +73,9 @@ module Banzai ...@@ -73,8 +73,9 @@ module Banzai
return unless node.has_attribute?('href') return unless node.has_attribute?('href')
begin begin
node['href'] = node['href'].strip
uri = Addressable::URI.parse(node['href']) uri = Addressable::URI.parse(node['href'])
uri.scheme = uri.scheme.strip.downcase if uri.scheme uri.scheme = uri.scheme.downcase if uri.scheme
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
......
...@@ -7,6 +7,11 @@ module Gitlab ...@@ -7,6 +7,11 @@ module Gitlab
new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user)) new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user))
end end
# TODO support the username field in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/628
def self.from_gitaly(gitaly_user)
new('', gitaly_user.name, gitaly_user.email, gitaly_user.gl_id)
end
def initialize(username, name, email, gl_id) def initialize(username, name, email, gl_id)
@username = username @username = username
@name = name @name = name
......
module Gitlab
module Git
class Wiki
DuplicatePageError = Class.new(StandardError)
CommitDetails = Struct.new(:name, :email, :message) do
def to_h
{ name: name, email: email, message: message }
end
end
def self.default_ref
'master'
end
# Initialize with a Gitlab::Git::Repository instance
def initialize(repository)
@repository = repository
end
def repository_exists?
@repository.exists?
end
def write_page(name, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
gollum_wiki.write_page(name, format, content, commit_details.to_h)
nil
rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
def delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails)
gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
nil
end
def update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
nil
end
def pages
gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
end
def page(title:, version: nil, dir: nil)
if version
version = Gitlab::Git::Commit.find(@repository, version).id
end
gollum_page = gollum_wiki.page(title, version, dir)
return unless gollum_page
new_page(gollum_page)
end
def file(name, version)
version ||= self.class.default_ref
gollum_file = gollum_wiki.file(name, version)
return unless gollum_file
Gitlab::Git::WikiFile.new(gollum_file)
end
def page_versions(page_path)
current_page = gollum_page_by_path(page_path)
current_page.versions.map do |gollum_git_commit|
gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id)
new_version(gollum_page, gollum_git_commit.id)
end
end
def preview_slug(title, format)
gollum_wiki.preview_page(title, '', format).url_path
end
private
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first
gollum_wiki.paged(page_name, page_dir)
end
def new_page(gollum_page)
Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id))
end
def new_version(gollum_page, commit_id)
commit = Gitlab::Git::Commit.find(@repository, commit_id)
Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format)
end
def assert_type!(object, klass)
unless object.is_a?(klass)
raise ArgumentError, "expected a #{klass}, got #{object.inspect}"
end
end
end
end
end
module Gitlab
module Git
class WikiFile
attr_reader :mime_type, :raw_data, :name
# This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab.
#
# Because Gollum::File is not serializable we must get all the data from
# 'gollum_file' during initialization, and NOT store it in an instance
# variable.
def initialize(gollum_file)
@mime_type = gollum_file.mime_type
@raw_data = gollum_file.raw_data
@name = gollum_file.name
end
end
end
end
module Gitlab
module Git
class WikiPage
attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical
# This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab.
#
# Because Gollum::Page is not serializable we must get all the data from
# 'gollum_page' during initialization, and NOT store it in an instance
# variable.
#
# Note that 'version' is a WikiPageVersion instance which it itself
# serializable. That means it's OK to store 'version' in an instance
# variable.
def initialize(gollum_page, version)
@url_path = gollum_page.url_path
@title = gollum_page.title
@format = gollum_page.format
@path = gollum_page.path
@raw_data = gollum_page.raw_data
@name = gollum_page.name
@historical = gollum_page.historical?
@version = version
end
def historical?
@historical
end
def text_data
return @text_data if defined?(@text_data)
@text_data = @raw_data && Gitlab::EncodingHelper.encode!(@raw_data.dup)
end
end
end
end
module Gitlab
module Git
class WikiPageVersion
attr_reader :commit, :format
# This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab.
#
# Both 'commit' (a Gitlab::Git::Commit) and 'format' (a string) are
# serializable.
def initialize(commit, format)
@commit = commit
@format = format
end
delegate :message, :sha, :id, :author_name, :authored_date, to: :commit
end
end
end
...@@ -233,6 +233,8 @@ module Gitlab ...@@ -233,6 +233,8 @@ module Gitlab
end end
def self.encode(s) def self.encode(s)
return "" if s.nil?
s.dup.force_encoding(Encoding::ASCII_8BIT) s.dup.force_encoding(Encoding::ASCII_8BIT)
end end
......
...@@ -274,7 +274,7 @@ module Gitlab ...@@ -274,7 +274,7 @@ module Gitlab
repository: @gitaly_repo, repository: @gitaly_repo,
left_commit_id: from_id, left_commit_id: from_id,
right_commit_id: to_id, right_commit_id: to_id,
paths: options.fetch(:paths, []).map { |path| GitalyClient.encode(path) } paths: options.fetch(:paths, []).compact.map { |path| GitalyClient.encode(path) }
} }
end end
......
...@@ -113,7 +113,7 @@ module Gitlab ...@@ -113,7 +113,7 @@ module Gitlab
def kubeconfig_embed_ca_pem(config, ca_pem) def kubeconfig_embed_ca_pem(config, ca_pem)
cluster = config.dig(:clusters, 0, :cluster) cluster = config.dig(:clusters, 0, :cluster)
cluster[:'certificate-authority-data'] = Base64.encode64(ca_pem) cluster[:'certificate-authority-data'] = Base64.strict_encode64(ca_pem)
end end
end end
end end
module Gitlab module Gitlab
class UrlSanitizer class UrlSanitizer
ALLOWED_SCHEMES = %w[http https ssh git].freeze
def self.sanitize(content) def self.sanitize(content)
regexp = URI::Parser.new.make_regexp(%w(http https ssh git)) regexp = URI::Parser.new.make_regexp(ALLOWED_SCHEMES)
content.gsub(regexp) { |url| new(url).masked_url } content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
...@@ -11,9 +13,9 @@ module Gitlab ...@@ -11,9 +13,9 @@ module Gitlab
def self.valid?(url) def self.valid?(url)
return false unless url.present? return false unless url.present?
Addressable::URI.parse(url.strip) uri = Addressable::URI.parse(url.strip)
true ALLOWED_SCHEMES.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
false false
end end
......
...@@ -89,6 +89,13 @@ module Gitlab ...@@ -89,6 +89,13 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
raise "Repository or ref not found" if params.empty? raise "Repository or ref not found" if params.empty?
if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive)
params.merge!(
'GitalyServer' => gitaly_server_hash(repository),
'GitalyRepository' => repository.gitaly_repository.to_h
)
end
[ [
SEND_DATA_HEADER, SEND_DATA_HEADER,
"git-archive:#{encode(params)}" "git-archive:#{encode(params)}"
......
...@@ -5,6 +5,7 @@ module SystemCheck ...@@ -5,6 +5,7 @@ module SystemCheck
# whitelisted as it may change the SSH client's behaviour dramatically. # whitelisted as it may change the SSH client's behaviour dramatically.
WHITELIST = %w[ WHITELIST = %w[
authorized_keys authorized_keys
authorized_keys.lock
authorized_keys2 authorized_keys2
known_hosts known_hosts
].freeze ].freeze
......
...@@ -5,8 +5,8 @@ module QA ...@@ -5,8 +5,8 @@ module QA
def choose_repository_clone_http def choose_repository_clone_http
find('#clone-dropdown').click find('#clone-dropdown').click
page.within('#clone-dropdown') do page.within('.clone-options-dropdown') do
find('span', text: 'HTTP').click click_link('HTTP')
end end
end end
......
...@@ -216,7 +216,7 @@ describe Projects::JobsController do ...@@ -216,7 +216,7 @@ describe Projects::JobsController do
expect(json_response['text']).to eq status.text expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon expect(json_response['icon']).to eq status.icon
expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
end end
end end
......
...@@ -658,7 +658,7 @@ describe Projects::MergeRequestsController do ...@@ -658,7 +658,7 @@ describe Projects::MergeRequestsController do
expect(json_response['text']).to eq status.text expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon expect(json_response['icon']).to eq status.icon
expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
end end
end end
......
...@@ -120,6 +120,40 @@ describe Projects::NotesController do ...@@ -120,6 +120,40 @@ describe Projects::NotesController do
expect(note_json[:diff_discussion_html]).to be_nil expect(note_json[:diff_discussion_html]).to be_nil
end end
end end
context 'with cross-reference system note', :request_store do
let(:new_issue) { create(:issue) }
let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
before do
note
create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
end
it 'filters notes that the user should not see' do
get :index, request_params
expect(parsed_response[:notes].count).to eq(1)
expect(note_json[:id]).to eq(note.id)
end
it 'does not result in N+1 queries' do
# Instantiate the controller variables to ensure QueryRecorder has an accurate base count
get :index, request_params
RequestStore.clear!
control_count = ActiveRecord::QueryRecorder.new do
get :index, request_params
end.count
RequestStore.clear!
create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
expect { get :index, request_params }.not_to exceed_query_limit(control_count)
end
end
end end
describe 'POST create' do describe 'POST create' do
......
...@@ -142,7 +142,7 @@ describe Projects::PipelinesController do ...@@ -142,7 +142,7 @@ describe Projects::PipelinesController do
expect(json_response['text']).to eq status.text expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon expect(json_response['icon']).to eq status.icon
expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
end end
end end
......
...@@ -289,6 +289,24 @@ describe ProjectsController do ...@@ -289,6 +289,24 @@ describe ProjectsController do
end end
end end
it 'updates Fast Forward Merge attributes' do
controller.instance_variable_set(:@project, project)
params = {
merge_method: :ff
}
put :update,
namespace_id: project.namespace,
id: project.id,
project: params
expect(response).to have_http_status(302)
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
end
def update_project(**parameters) def update_project(**parameters)
put :update, put :update,
namespace_id: project.namespace.path, namespace_id: project.namespace.path,
......
...@@ -446,7 +446,7 @@ describe 'Copy as GFM', js: true do ...@@ -446,7 +446,7 @@ describe 'Copy as GFM', js: true do
def verify(label, *gfms) def verify(label, *gfms)
aggregate_failures(label) do aggregate_failures(label) do
gfms.each do |gfm| gfms.each do |gfm|
html = gfm_to_html(gfm) html = gfm_to_html(gfm).gsub(/\A&#x000A;|&#x000A;\z/, '')
output_gfm = html_to_gfm(html) output_gfm = html_to_gfm(html)
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
...@@ -463,16 +463,15 @@ describe 'Copy as GFM', js: true do ...@@ -463,16 +463,15 @@ describe 'Copy as GFM', js: true do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
context 'from a diff' do context 'from a diff' do
before do shared_examples 'copying code from a diff' do
visit project_commit_path(project, sample_commit.id)
end
context 'selecting one word of text' do context 'selecting one word of text' do
it 'copies as inline code' do it 'copies as inline code' do
verify( verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no', '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
'`RuntimeError`' '`RuntimeError`',
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
) )
end end
end end
...@@ -480,9 +479,11 @@ describe 'Copy as GFM', js: true do ...@@ -480,9 +479,11 @@ describe 'Copy as GFM', js: true do
context 'selecting one line of text' do context 'selecting one line of text' do
it 'copies as inline code' do it 'copies as inline code' do
verify( verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line', '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]',
'`raise RuntimeError, "System commands must be given as an array of strings"`' '`raise RuntimeError, "System commands must be given as an array of strings"`',
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
) )
end end
end end
...@@ -498,11 +499,66 @@ describe 'Copy as GFM', js: true do ...@@ -498,11 +499,66 @@ describe 'Copy as GFM', js: true do
end end
``` ```
GFM GFM
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
) )
end end
end end
end end
context 'inline diff' do
before do
visit project_commit_path(project, sample_commit.id, view: 'inline')
end
it_behaves_like 'copying code from a diff'
end
context 'parallel diff' do
before do
visit project_commit_path(project, sample_commit.id, view: 'parallel')
end
it_behaves_like 'copying code from a diff'
context 'selecting code on the left' do
it 'copies as a code block' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
<<-GFM.strip_heredoc,
```ruby
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
```
GFM
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].left-side'
)
end
end
context 'selecting code on the right' do
it 'copies as a code block' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
<<-GFM.strip_heredoc,
```ruby
unless cmd.is_a?(Array)
raise RuntimeError, "System commands must be given as an array of strings"
end
```
GFM
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].right-side'
)
end
end
end
end
context 'from a blob' do context 'from a blob' do
before do before do
visit project_blob_path(project, File.join('master', 'files/ruby/popen.rb')) visit project_blob_path(project, File.join('master', 'files/ruby/popen.rb'))
...@@ -587,9 +643,9 @@ describe 'Copy as GFM', js: true do ...@@ -587,9 +643,9 @@ describe 'Copy as GFM', js: true do
end end
end end
def verify(selector, gfm) def verify(selector, gfm, target: nil)
html = html_for_selector(selector) html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection') output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
end end
...@@ -605,15 +661,21 @@ describe 'Copy as GFM', js: true do ...@@ -605,15 +661,21 @@ describe 'Copy as GFM', js: true do
page.evaluate_script(js) page.evaluate_script(js)
end end
def html_to_gfm(html, transformer = 'transformGFMSelection') def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<-JS.strip_heredoc js = <<-JS.strip_heredoc
(function(html) { (function(html) {
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}]; var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div'); var node = document.createElement('div');
node.innerHTML = html; $(html).each(function() { node.appendChild(this) });
var targetSelector = #{target.to_json};
var target;
if (targetSelector) {
target = document.querySelector(targetSelector);
}
node = transformer(node); node = transformer(node, target);
if (!node) return null; if (!node) return null;
return window.gl.CopyAsGFM.nodeToGFM(node); return window.gl.CopyAsGFM.nodeToGFM(node);
......
...@@ -218,54 +218,15 @@ describe 'New/edit issue', :js do ...@@ -218,54 +218,15 @@ describe 'New/edit issue', :js do
context 'edit issue' do context 'edit issue' do
before do before do
visit edit_project_issue_path(project, issue) visit project_issue_path(project, issue)
end page.within('.content .issuable-actions') do
click_on 'Edit'
it 'allows user to update issue' do
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
page.within '.js-user-search' do
expect(page).to have_content user.name
end
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
end
click_button 'Labels'
page.within '.dropdown-menu-labels' do
click_link label.title
click_link label2.title
end
page.within '.js-label-select' do
expect(page).to have_content label.title
end
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Save changes'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content user.name
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
page.within '.labels' do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
end end
end end
it 'description has autocomplete' do it 'description has autocomplete' do
find('#issue_description').native.send_keys('') find_field('issue-description').native.send_keys('')
fill_in 'issue_description', with: '@' fill_in 'issue-description', with: '@'
expect(page).to have_selector('.atwho-view') expect(page).to have_selector('.atwho-view')
end end
......
require 'spec_helper' require 'spec_helper'
describe 'Issues' do describe 'Issues', :js do
include DropzoneHelper include DropzoneHelper
include IssueHelpers include IssueHelpers
include SortingHelper include SortingHelper
...@@ -24,109 +24,15 @@ describe 'Issues' do ...@@ -24,109 +24,15 @@ describe 'Issues' do
end end
before do before do
visit edit_project_issue_path(project, issue) visit project_issue_path(project, issue)
find('.js-zen-enter').click page.within('.content .issuable-actions') do
end find('.issuable-edit').click
it 'opens new issue popup' do
expect(page).to have_content("Issue ##{issue.iid}")
end
end
describe 'Editing issue assignee' do
let!(:issue) do
create(:issue,
author: user,
assignees: [user],
project: project)
end
it 'allows user to select unassigned', js: true do
visit edit_project_issue_path(project, issue)
expect(page).to have_content "Assignee #{user.name}"
first('.js-user-search').click
click_link 'Unassigned'
click_button 'Save changes'
page.within('.assignee') do
expect(page).to have_content 'No assignee - assign yourself'
end
expect(issue.reload.assignees).to be_empty
end
end
describe 'due date', js: true do
context 'on new form' do
before do
visit new_project_issue_path(project)
end
it 'saves with due date' do
date = Date.today.at_beginning_of_month
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.pika-single' do
click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Submit issue'
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
end
end
end
context 'on edit form' do
let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
before do
visit edit_project_issue_path(project, issue)
end
it 'saves with due date' do
date = Date.today.at_beginning_of_month
expect(find('#issuable-due-date').value).to eq date.to_s
date = date.tomorrow
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.pika-single' do
click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Save changes'
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
end end
find('.issue-details .content-block .js-zen-enter').click
end end
it 'warns about version conflict' do it 'opens new issue popup' do
issue.update(title: "New title") expect(page).to have_content(issue.description)
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
click_button 'Save changes'
expect(page).to have_content 'Someone edited the issue the same time you did'
end
end end
end end
......
...@@ -84,7 +84,7 @@ feature 'Diff note avatars', js: true do ...@@ -84,7 +84,7 @@ feature 'Diff note avatars', js: true do
end end
it 'shows note avatar' do it 'shows note avatar' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
...@@ -92,7 +92,7 @@ feature 'Diff note avatars', js: true do ...@@ -92,7 +92,7 @@ feature 'Diff note avatars', js: true do
end end
it 'shows comment on note avatar' do it 'shows comment on note avatar' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
...@@ -100,13 +100,13 @@ feature 'Diff note avatars', js: true do ...@@ -100,13 +100,13 @@ feature 'Diff note avatars', js: true do
end end
it 'toggles comments when clicking avatar' do it 'toggles comments when clicking avatar' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
end end
expect(page).to have_selector('.notes_holder', visible: false) expect(page).to have_selector('.notes_holder', visible: false)
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
first('img.js-diff-comment-avatar').click first('img.js-diff-comment-avatar').click
end end
...@@ -122,7 +122,7 @@ feature 'Diff note avatars', js: true do ...@@ -122,7 +122,7 @@ feature 'Diff note avatars', js: true do
wait_for_requests wait_for_requests
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
expect(page).not_to have_selector('img.js-diff-comment-avatar') expect(page).not_to have_selector('img.js-diff-comment-avatar')
end end
end end
...@@ -138,7 +138,7 @@ feature 'Diff note avatars', js: true do ...@@ -138,7 +138,7 @@ feature 'Diff note avatars', js: true do
wait_for_requests wait_for_requests
end end
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').trigger('click') find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
...@@ -158,7 +158,7 @@ feature 'Diff note avatars', js: true do ...@@ -158,7 +158,7 @@ feature 'Diff note avatars', js: true do
end end
end end
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').trigger('click') find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
...@@ -176,7 +176,7 @@ feature 'Diff note avatars', js: true do ...@@ -176,7 +176,7 @@ feature 'Diff note avatars', js: true do
end end
it 'shows extra comment count' do it 'shows extra comment count' do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(find('.diff-comments-more-count')).to have_content '+1' expect(find('.diff-comments-more-count')).to have_content '+1'
...@@ -185,4 +185,10 @@ feature 'Diff note avatars', js: true do ...@@ -185,4 +185,10 @@ feature 'Diff note avatars', js: true do
end end
end end
end end
def find_line(line_code)
line = find("[id='#{line_code}']")
line = line.find(:xpath, 'preceding-sibling::*[1][self::td]') if line.tag_name == 'td'
line
end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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