Commit 710422a0 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-08-30

* upstream/master: (103 commits)
  Fix broken Frontend JS guide
  Replace 'project/star.feature' spinach test with an rspec analog
  Adds position fixed to right sidebar
  Decrease Metrics/CyclomaticComplexity threshold to 15
  Prevents rendering empty badge when pipeline request fails
  Make file encoding dropdown consistent
  Fixes the diff changes buttons from toggling when scrolling
  exclude spec/ and features/ from `Style/PredicateName` cop
  Resolve "Monitoring graph date formatting is wrong"
  add changelog
  replace `is_ancestor` with `ancestor?`
  replace `is_member_of` with `member_of?`
  replace `has_n_stars` with `has_n_stars?`
  replace `has_matching_label` with `has_matching_label?`
  replace `is_team_member?` with `team_member?`
  replace `is_spam?` with `spam?`
  replace `is_default_branch?` with `default_branch?`
  replace `is_successful?` with `successful?`
  replace `is_multi_check?` with `multi_check?`
  replace `is_gitlab_user?` with `gitlab_user?`
  ...
parents f0deea60 499b6406
......@@ -74,19 +74,6 @@ stages:
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
.only-if-want-mysql: &only-if-want-mysql
only:
- /mysql/
- /-stable/
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
- tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee
- tags@gitlab/gitlabhq
- tags@gitlab/gitlab-ee
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
......@@ -130,7 +117,6 @@ stages:
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
<<: *only-if-want-mysql
<<: *except-docs
.spinach-metadata: &spinach-metadata
......@@ -162,7 +148,6 @@ stages:
.spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata
<<: *use-mysql
<<: *only-if-want-mysql
<<: *except-docs
.only-canonical-masters: &only-canonical-masters
......
......@@ -608,6 +608,18 @@ Style/YodaCondition:
Style/Proc:
Enabled: true
# Use `spam?` instead of `is_spam?`
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
# NameWhitelist: is_a?
Style/PredicateName:
Enabled: true
NamePrefixBlacklist: is_
Exclude:
- 'spec/**/*'
- 'features/**/*'
# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
......@@ -633,7 +645,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
Max: 16
Max: 15
# Limit lines to 80 characters.
Metrics/LineLength:
......@@ -1186,6 +1198,10 @@ GitlabSecurity/DeepMunge:
- 'lib/**/*.rake'
- 'spec/**/*'
# To be enabled by https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13610
GitlabSecurity/JsonSerialization:
Enabled: false
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
......
......@@ -237,14 +237,6 @@ Style/PercentLiteralDelimiters:
Style/PerlBackrefs:
Enabled: false
# Offense count: 105
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
# NameWhitelist: is_a?
Style/PredicateName:
Enabled: false
# Offense count: 58
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
......
......@@ -349,7 +349,7 @@ group :development, :test do
gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.1', require: false
gem 'rubocop-gitlab-security', '~> 0.0.6', require: false
gem 'rubocop-gitlab-security', '~> 0.1.0', require: false
gem 'scss_lint', '~> 0.54.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
......
......@@ -798,7 +798,7 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.0.6)
rubocop-gitlab-security (0.1.0)
rubocop (>= 0.47.1)
rubocop-rspec (1.15.1)
rubocop (>= 0.42.0)
......@@ -1163,7 +1163,7 @@ DEPENDENCIES
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.49.1)
rubocop-gitlab-security (~> 0.0.6)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.15.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
......@@ -1213,4 +1213,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.15.3
1.15.4
......@@ -211,13 +211,13 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
case 'groups:merge_requests':
new ProjectSelect();
initLegacyFilters();
break;
case 'groups:issues':
case 'groups:merge_requests':
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('issues');
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
new ProjectSelect();
......
export const isSticky = (el, scrollY, stickyTop) => {
const top = el.offsetTop - scrollY;
const top = Math.floor(el.offsetTop - scrollY);
if (top <= stickyTop) {
el.classList.add('is-stuck');
......
......@@ -253,6 +253,7 @@ import bp from './breakpoints';
loadDiff(source) {
if (this.diffsLoaded) {
document.dispatchEvent(new CustomEvent('scroll'));
return;
}
......
......@@ -7,6 +7,7 @@
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
import { timeScaleFormat } from '../utils/date_time_formatters';
import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
......@@ -159,6 +160,7 @@
const xAxis = d3.svg.axis()
.scale(axisXScale)
.ticks(measurements.xTicks)
.tickFormat(timeScaleFormat)
.orient('bottom');
const yAxis = d3.svg.axis()
......@@ -266,14 +268,6 @@
stroke-width="2"
transform="translate(-5, 20)">
</path>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<monitoring-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
......@@ -289,6 +283,14 @@
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
</svg>
</svg>
</div>
......
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
import { dateFormat, timeFormat } from '../utils/date_time_formatters';
export default {
props: {
......@@ -58,7 +55,7 @@
class="deploy-info"
v-if="showDeployInfo">
<g
v-for="(deployment, index) in deploymentData"
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
......@@ -92,7 +89,7 @@
width="90"
height="58">
</rect>
<g
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
......
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
import { dateFormat, timeFormat } from '../utils/date_time_formatters';
export default {
props: {
......@@ -72,7 +69,7 @@
r="5"
transform="translate(-5, 20)">
</circle>
<svg
<svg
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
......
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p');
export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()],
[':%S', d => d.getSeconds()],
['%-I:%M', d => d.getMinutes()],
['%-I %p', d => d.getHours()],
['%a %-d', d => d.getDay() && d.getDate() !== 1],
['%b %-d', d => d.getDate() !== 1],
['%B', d => d.getMonth()],
['%Y', () => true],
]);
......@@ -47,7 +47,6 @@ export default class NewNavSidebar {
if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-new-sidebar', !collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
......
<script>
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true,
},
},
count: {
type: Object,
required: true,
mounted() {
$(document).trigger('init.scrolling-tabs');
},
paths: {
type: Object,
required: true,
methods: {
shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
};
</script>
<template>
......@@ -27,7 +33,9 @@ export default {
:class="{ active: scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">
<span
v-if="shouldRenderBadge(count.all)"
class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
......@@ -37,7 +45,9 @@ export default {
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span class="badge">
<span
v-if="shouldRenderBadge(count.pending)"
class="badge">
{{count.pending}}
</span>
</a>
......@@ -47,7 +57,9 @@ export default {
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span class="badge">
<span
v-if="shouldRenderBadge(count.running)"
class="badge">
{{count.running}}
</span>
</a>
......@@ -57,7 +69,9 @@ export default {
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span class="badge">
<span
v-if="shouldRenderBadge(count.finished)"
class="badge">
{{count.finished}}
</span>
</a>
......
......@@ -278,7 +278,9 @@
}
// TODO: change global style
.ajax-project-dropdown {
.ajax-project-dropdown,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
color: $gl-text-color;
}
......
......@@ -120,6 +120,7 @@ $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
$gl-text-red: $red-500;
$gl-text-orange: $orange-600;
$gl-link-color: $blue-600;
......
......@@ -102,6 +102,8 @@
}
.member-search-form {
@include new-style-dropdown;
position: relative;
@media (min-width: $screen-sm-min) {
......
......@@ -475,6 +475,8 @@
}
.mr-source-target {
@include new-style-dropdown;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
......
......@@ -766,17 +766,25 @@ ul.notes {
background-color: transparent;
border: none;
outline: 0;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
cursor: default;
}
&:not(.is-disabled):hover,
&:not(.is-disabled) {
&:hover,
&:focus {
color: $gl-text-green;
}
}
&.is-active {
color: $gl-text-green;
svg {
fill: $gl-text-green;
&:hover,
&:focus {
color: $gl-text-green-hover;
}
}
......
......@@ -99,6 +99,30 @@
.blob-viewer-container {
flex: 1;
overflow: auto;
> div,
.file-content {
display: flex;
}
> div,
.file-content,
.blob-viewer,
.line-number,
.blob-content,
.code {
min-height: 100%;
width: 100%;
}
.line-numbers {
min-width: 44px;
}
.blob-content {
flex: 1;
overflow-x: auto;
}
}
#tabs {
......
......@@ -265,3 +265,7 @@
font-weight: $gl-font-weight-bold;
}
}
.todos-filters {
@include new-style-dropdown;
}
......@@ -35,13 +35,13 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def edit
render_404 if @milestone.is_legacy_group_milestone?
render_404 if @milestone.legacy_group_milestone?
end
def update
# Keep this compatible with legacy group milestones where we have to update
# all projects milestones states at once.
if @milestone.is_legacy_group_milestone?
if @milestone.legacy_group_milestone?
update_params = milestone_params.select { |key| key == "state_event" }
milestones = @milestone.milestones
else
......@@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_path
if @milestone.is_legacy_group_milestone?
if @milestone.legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
else
group_milestone_path(group, @milestone.iid)
......
......@@ -205,7 +205,7 @@ class Projects::IssuesController < Projects::ApplicationController
task_status: @issue.task_status
}
if @issue.is_edited?
if @issue.edited?
response[:updated_at] = @issue.updated_at
response[:updated_by_name] = @issue.last_edited_by.name
response[:updated_by_path] = user_path(@issue.last_edited_by)
......
......@@ -327,14 +327,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@merge_request.merge_async(current_user.id, params)
:success
else
:failed
end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@merge_request.merge_async(current_user.id, params)
:success
end
......
......@@ -181,7 +181,7 @@ module ApplicationHelper
end
def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false)
return unless object.is_edited?
return unless object.edited?
content_tag :small, class: 'edited-text' do
output = content_tag(:span, 'Edited ')
......
......@@ -237,7 +237,7 @@ module IssuablesHelper
end
def updated_at_by(issuable)
return {} unless issuable.is_edited?
return {} unless issuable.edited?
{
updatedAt: issuable.updated_at.to_time.iso8601,
......
......@@ -196,7 +196,7 @@ module MilestonesHelper
def group_milestone_route(milestone, params = {})
params = nil if params.empty?
if milestone.is_legacy_group_milestone?
if milestone.legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
group_milestone_path(@group, milestone.iid, milestone: params)
......
module MilestonesRoutingHelper
def milestone_path(milestone, *args)
if milestone.is_group_milestone?
if milestone.group_milestone?
group_milestone_path(milestone.group, milestone, *args)
elsif milestone.is_project_milestone?
elsif milestone.project_milestone?
project_milestone_path(milestone.project, milestone, *args)
end
end
def milestone_url(milestone, *args)
if milestone.is_group_milestone?
if milestone.group_milestone?
group_milestone_url(milestone.group, milestone, *args)
elsif milestone.is_project_milestone?
elsif milestone.project_milestone?
project_milestone_url(milestone.project, milestone, *args)
end
end
......
......@@ -143,7 +143,7 @@ module Ci
expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
end
def is_runner_queue_value_latest?(value)
def runner_queue_value_latest?(value)
ensure_runner_queue_value == value if value.present?
end
......
module Editable
extend ActiveSupport::Concern
def is_edited?
def edited?
last_edited_at.present? && last_edited_at != created_at
end
......
......@@ -70,19 +70,19 @@ module Milestoneish
due_date && due_date.past?
end
def is_group_milestone?
def group_milestone?
false
end
def is_project_milestone?
def project_milestone?
false
end
def is_legacy_group_milestone?
def legacy_group_milestone?
false
end
def is_dashboard_milestone?
def dashboard_milestone?
false
end
......
......@@ -23,7 +23,7 @@ module ProtectedRef
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
......
......@@ -3,7 +3,7 @@ class DashboardMilestone < GlobalMilestone
{ authorized_only: true }
end
def is_dashboard_milestone?
def dashboard_milestone?
true
end
end
......@@ -49,7 +49,7 @@ class Deployment < ActiveRecord::Base
# created before then could have a `sha` referring to a commit that no
# longer exists in the repository, so just ignore those.
begin
project.repository.is_ancestor?(commit.id, sha)
project.repository.ancestor?(commit.id, sha)
rescue Rugged::OdbError
false
end
......
......@@ -248,13 +248,17 @@ class Group < Namespace
SystemHooksService.new
end
<<<<<<< HEAD
def first_non_empty_project
projects.detect { |project| !project.empty_repo? }
end
def refresh_members_authorized_projects
=======
def refresh_members_authorized_projects(blocking: true)
>>>>>>> upstream/master
UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
.execute
.execute(blocking: blocking)
end
def user_ids_for_project_authorizations
......
......@@ -19,7 +19,7 @@ class GroupMilestone < GlobalMilestone
{ group_id: group.id }
end
def is_legacy_group_milestone?
def legacy_group_milestone?
true
end
end
......@@ -249,6 +249,14 @@ class MergeRequest < ActiveRecord::Base
end
end
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
jid = MergeWorker.perform_async(id, user_id, params)
update_column(:merge_jid, jid)
end
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
......@@ -392,9 +400,7 @@ class MergeRequest < ActiveRecord::Base
end
def merge_ongoing?
return false unless merge_jid
Gitlab::SidekiqStatus.num_running([merge_jid]) > 0
!!merge_jid && !merged?
end
def closed_without_fork?
......@@ -844,7 +850,7 @@ class MergeRequest < ActiveRecord::Base
lock_mr
yield
ensure
unlock_mr if locked?
unlock_mr
end
end
......
......@@ -167,7 +167,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
return if is_group_milestone? && format != :name
return if group_milestone? && format != :name
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
......@@ -211,11 +211,11 @@ class Milestone < ActiveRecord::Base
group || project
end
def is_group_milestone?
def group_milestone?
group_id.present?
end
def is_project_milestone?
def project_milestone?
project_id.present?
end
......
......@@ -152,14 +152,14 @@ module Network
end
def find_free_parent_space(range, space_base, space_step, space_default)
if is_overlap?(range, space_default)
if overlap?(range, space_default)
find_free_space(range, space_step, space_base, space_default)
else
space_default
end
end
def is_overlap?(range, overlap_space)
def overlap?(range, overlap_space)
range.each do |i|
if i != range.first &&
i != range.last &&
......
......@@ -27,46 +27,45 @@ class NotificationRecipient
@notification_setting ||= find_notification_setting
end
def raw_notification_level
notification_setting&.level&.to_sym
end
def notification_level
# custom is treated the same as watch if it's enabled - otherwise it's
# set to :custom, meaning to send exactly when our type is :participating
# or :mention.
@notification_level ||=
case raw_notification_level
when :custom
if @custom_action && notification_setting&.event_enabled?(@custom_action)
:watch
else
:custom
end
else
raw_notification_level
end
@notification_level ||= notification_setting&.level&.to_sym
end
def notifiable?
return false unless has_access?
return false if own_activity?
return true if @type == :subscription
return false if notification_level.nil? || notification_level == :disabled
return %i[participating mention].include?(@type) if notification_level == :custom
# even users with :disabled notifications receive manual subscriptions
return !unsubscribed? if @type == :subscription
return false if %i[watch participating].include?(notification_level) && excluded_watcher_action?
return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[@type]
return false unless suitable_notification_level?
# check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed
return false if unsubscribed?
true
end
def suitable_notification_level?
case notification_level
when :disabled, nil
false
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch, :participating
!excluded_watcher_action?
when :mention
@type == :mention
else
false
end
end
def custom_enabled?
@custom_action && notification_setting&.event_enabled?(@custom_action)
end
def unsubscribed?
return false unless @target
return false unless @target.respond_to?(:subscriptions)
......@@ -98,7 +97,7 @@ class NotificationRecipient
def excluded_watcher_action?
return false unless @custom_action
return false if raw_notification_level == :custom
return false if notification_level == :custom
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
......
......@@ -101,9 +101,9 @@ class ChatNotificationService < Service
when "push", "tag_push"
ChatMessage::PushMessage.new(data)
when "issue"
ChatMessage::IssueMessage.new(data) unless is_update?(data)
ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
ChatMessage::MergeMessage.new(data) unless is_update?(data)
ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
when "pipeline"
......@@ -136,7 +136,7 @@ class ChatNotificationService < Service
project.web_url
end
def is_update?(data)
def update?(data)
data[:object_attributes][:action] == 'update'
end
......
......@@ -85,9 +85,9 @@ class HipchatService < Service
when "push", "tag_push"
create_push_message(data)
when "issue"
create_issue_message(data) unless is_update?(data)
create_issue_message(data) unless update?(data)
when "merge_request"
create_merge_request_message(data) unless is_update?(data)
create_merge_request_message(data) unless update?(data)
when "note"
create_note_message(data)
when "pipeline"
......@@ -283,7 +283,7 @@ class HipchatService < Service
"<a href=\"#{project_url}\">#{project_name}</a>"
end
def is_update?(data)
def update?(data)
data[:object_attributes][:action] == 'update'
end
......
......@@ -970,7 +970,7 @@ class Repository
if branch_commit
same_head = branch_commit.id == root_ref_commit.id
!same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
!same_head && ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
......@@ -1032,12 +1032,12 @@ class Repository
nil
end
def is_ancestor?(ancestor_id, descendant_id)
def ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil?
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id)
raw_repository.ancestor?(ancestor_id, descendant_id)
else
rugged_is_ancestor?(ancestor_id, descendant_id)
end
......
......@@ -19,13 +19,13 @@ class ProjectPolicy < BasePolicy
desc "Project has public builds enabled"
condition(:public_builds, scope: :subject) { project.public_builds? }
# For guest access we use #is_team_member? so we can use
# For guest access we use #team_member? so we can use
# project.members, which gets cached in subject scope.
# This is safe because team_access_level is guaranteed
# by ProjectAuthorization's validation to be at minimum
# GUEST
desc "User has guest access"
condition(:guest) { is_team_member? }
condition(:guest) { team_member? }
desc "User has reporter access"
condition(:reporter) { team_access_level >= Gitlab::Access::REPORTER }
......@@ -296,7 +296,7 @@ class ProjectPolicy < BasePolicy
private
def is_team_member?
def team_member?
return false if @user.nil?
greedy_load_subject = false
......
......@@ -7,7 +7,7 @@ class AkismetService
@options = options
end
def is_spam?
def spam?
return false unless akismet_enabled?
params = {
......
......@@ -15,7 +15,7 @@ module Ci
pipeline_schedule: schedule
)
result = validate(current_user || trigger_request.trigger.owner,
result = validate(current_user,
ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors,
mirror_update: mirror_update)
......
......@@ -30,7 +30,7 @@ class GitPushService < BaseService
@project.repository.after_create_branch
# Re-find the pushed commits.
if is_default_branch?
if default_branch?
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
process_default_branch
else
......@@ -50,7 +50,7 @@ class GitPushService < BaseService
# Update the bare repositories info/attributes file using the contents of the default branches
# .gitattributes file
update_gitattributes if is_default_branch?
update_gitattributes if default_branch?
end
if current_application_settings.elasticsearch_indexing? && is_default_branch?
......@@ -71,7 +71,7 @@ class GitPushService < BaseService
end
def update_caches
if is_default_branch?
if default_branch?
if push_to_new_branch?
# If this is the initial push into the default branch, the file type caches
# will already be reset as a result of `Project#change_head`.
......@@ -113,7 +113,7 @@ class GitPushService < BaseService
# Schedules processing of commit messages.
def process_commit_messages
default = is_default_branch?
default = default_branch?
@push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
if commit.matches_cross_reference_regex?
......@@ -216,7 +216,7 @@ class GitPushService < BaseService
Gitlab::Git.branch_ref?(params[:ref])
end
def is_default_branch?
def default_branch?
Gitlab::Git.branch_ref?(params[:ref]) &&
(Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?)
end
......
......@@ -33,10 +33,12 @@ module MergeRequests
merge_request.in_locked_state do
if commit
after_merge
clean_merge_jid
success
end
end
rescue MergeError => e
clean_merge_jid
log_merge_error(e.message, save_message_on_model: true)
end
......@@ -99,6 +101,10 @@ module MergeRequests
end
end
def clean_merge_jid
merge_request.update_column(:merge_jid, nil)
end
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
......
......@@ -30,7 +30,7 @@ module MergeRequests
next
end
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
end
end
......
......@@ -95,7 +95,7 @@ module MergeRequests
if merge_request.head_pipeline && merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
else
MergeWorker.perform_async(merge_request.id, current_user.id, {})
merge_request.merge_async(current_user.id, {})
end
end
......
module Milestones
class CloseService < Milestones::BaseService
def execute(milestone)
if milestone.close && milestone.is_project_milestone?
if milestone.close && milestone.project_milestone?
event_service.close_milestone(milestone, current_user)
end
......
......@@ -3,7 +3,7 @@ module Milestones
def execute
milestone = parent.milestones.new(params)
if milestone.save && milestone.is_project_milestone?
if milestone.save && milestone.project_milestone?
event_service.open_milestone(milestone, current_user)
end
......
module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
return unless milestone.is_project_milestone?
return unless milestone.project_milestone?
Milestone.transaction do
update_params = { milestone: nil }
......
module Milestones
class ReopenService < Milestones::BaseService
def execute(milestone)
if milestone.activate && milestone.is_project_milestone?
if milestone.activate && milestone.project_milestone?
event_service.reopen_milestone(milestone, current_user)
end
......
......@@ -95,7 +95,7 @@ module NotificationRecipientService
def add_participants(user)
return unless target.respond_to?(:participants)
self << [target.participants(user), :watch]
self << [target.participants(user), :participating]
end
# Get project/group users with CUSTOM notification level
......
......@@ -102,15 +102,26 @@ module Projects
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
unless @project.group || @project.gitlab_project_import?
owners = [current_user, @project.namespace.owner].compact.uniq
@project.add_master(owners, current_user: current_user)
end
setup_authorizations
end
<<<<<<< HEAD
# EE-only
create_predefined_push_rule
@project.group&.refresh_members_authorized_projects
=======
# Refresh the current user's authorizations inline (so they can access the
# project immediately after this request completes), and any other affected
# users in the background
def setup_authorizations
if @project.group
@project.group.refresh_members_authorized_projects(blocking: false)
current_user.refresh_authorized_projects
else
@project.add_master(@project.namespace.owner, current_user: current_user)
end
>>>>>>> upstream/master
end
def skip_wiki?
......
......@@ -45,7 +45,7 @@ class SpamService
def check(api)
return false unless request && check_for_spam?
return false unless akismet.is_spam?
return false unless akismet.spam?
create_spam_log(api)
true
......
......@@ -142,7 +142,7 @@ module SystemNoteService
#
# Returns the created Note object
def change_milestone(noteable, project, author, milestone)
format = milestone&.is_group_milestone? ? :name : :iid
format = milestone&.group_milestone? ? :name : :iid
body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
......
......@@ -2,47 +2,16 @@ module TestHooks
class SystemService < TestHooks::BaseService
private
def project
@project ||= begin
project = Project.first
throw(:validation_error, 'Ensure that at least one project exists.') unless project
project
end
end
def push_events_data
if project.empty_repo?
throw(:validation_error, "Ensure project \"#{project.human_name}\" has commits.")
end
Gitlab::DataBuilder::Push.build_sample(project, current_user)
Gitlab::DataBuilder::Push.sample_data
end
def tag_push_events_data
if project.repository.tags.empty?
throw(:validation_error, "Ensure project \"#{project.human_name}\" has tags.")
end
Gitlab::DataBuilder::Push.build_sample(project, current_user)
Gitlab::DataBuilder::Push.sample_data
end
def repository_update_events_data
commit = project.commit
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
unless commit
throw(:validation_error, "Ensure project \"#{project.human_name}\" has commits.")
end
change = Gitlab::DataBuilder::Repository.single_change(
commit.parent_id || Gitlab::Git::BLANK_SHA,
commit.id,
ref
)
Gitlab::DataBuilder::Repository.update(project, current_user, [change], [ref])
Gitlab::DataBuilder::Repository.sample_data
end
end
end
......@@ -5,7 +5,13 @@ class UserProjectAccessChangedService
@user_ids = Array.wrap(user_ids)
end
def execute
AuthorizedProjectsWorker.bulk_perform_and_wait(@user_ids.map { |id| [id] })
def execute(blocking: true)
bulk_args = @user_ids.map { |id| [id] }
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
AuthorizedProjectsWorker.bulk_perform_async(bulk_args)
end
end
end
- page_title "Merge Requests"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
- if show_new_nav? && current_user
- content_for :breadcrumbs_extra do
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
......@@ -13,7 +17,7 @@
.nav-controls{ class: ("visible-xs" if show_new_nav?) }
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/issuable/search_bar', type: :merge_requests
.row-content-block.second-block
Only merge requests from
......
= render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.is_legacy_group_milestone?
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.legacy_group_milestone?
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
- breadcrumb_link = breadcrumb_title_link
- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation" }
.breadcrumbs-container{ class: [container_class, @content_class] }
.breadcrumbs-container{ class: [container, @content_class] }
- if defined?(@new_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only Open sidebar
......
<<<<<<< HEAD
- board = local_assigns.fetch(:board, nil)
=======
- @no_breadcrumb_container = true
>>>>>>> upstream/master
- @no_container = true
- @content_class = "issue-boards-content js-focus-mode-board"
- page_title "Boards"
......
......@@ -68,9 +68,10 @@
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
.row
.col-lg-12
......
......@@ -26,8 +26,12 @@
":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
= icon('spin spinner', 'v-if' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-else' => '' }
%template{ 'v-if' => 'isResolved' }
= render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' }
= render 'shared/icons/icon_status_success.svg'
- if current_user
- if note.emoji_awardable?
......
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></svg>
......@@ -5,7 +5,7 @@
.row
.col-sm-6
%strong= link_to truncate(milestone.title, length: 100), milestone_path
- if milestone.is_group_milestone?
- if milestone.group_milestone?
%span - Group Milestone
- else
%span - Project Milestone
......@@ -18,10 +18,10 @@
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
- if milestone.is_a?(GlobalMilestone) || milestone.is_group_milestone?
- if milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
.row
.col-sm-6
- if milestone.is_legacy_group_milestone?
- if milestone.legacy_group_milestone?
.expiration= render('shared/milestone_expired', milestone: milestone)
.projects
- milestone.milestones.each do |milestone|
......@@ -31,7 +31,7 @@
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
- if milestone.is_group_milestone?
- if milestone.group_milestone?
= link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
......
......@@ -22,7 +22,7 @@
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
- if milestone.is_group_milestone?
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
- if milestone.active?
......@@ -33,7 +33,7 @@
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- if @milestone.is_group_milestone? && @milestone.description.present?
- if @milestone.group_milestone? && @milestone.description.present?
%div
.description
.wiki
......@@ -44,7 +44,7 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
- if @milestone.is_legacy_group_milestone? || @milestone.is_dashboard_milestone?
- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone?
.table-holder
%table.table
%thead
......@@ -67,7 +67,7 @@
Open
%td
= ms.expires_at
- elsif @milestone.is_group_milestone?
- elsif @milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
......
......@@ -4,20 +4,40 @@ class AuthorizedProjectsWorker
# Schedules multiple jobs and waits for them to be completed.
def self.bulk_perform_and_wait(args_list)
# Short-circuit: it's more efficient to do small numbers of jobs inline
return bulk_perform_inline(args_list) if args_list.size <= 3
waiter = Gitlab::JobWaiter.new(args_list.size)
# Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
# into [[1, "key"], [2, "key"], [3, "key"]]
waiting_args_list = args_list.map { |args| args << waiter.key }
waiting_args_list = args_list.map { |args| [*args, waiter.key] }
bulk_perform_async(waiting_args_list)
waiter.wait
end
# Schedules multiple jobs to run in sidekiq without waiting for completion
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
end
# Performs multiple jobs directly. Failed jobs will be put into sidekiq so
# they can benefit from retries
def self.bulk_perform_inline(args_list)
failed = []
args_list.each do |args|
begin
new.perform(*args)
rescue
failed << args
end
end
bulk_perform_async(failed) if failed.present?
end
def perform(user_id, notify_key = nil)
user = User.find_by(id: user_id)
......
class BuildCoverageWorker
include Sidekiq::Worker
include BuildQueue
include PipelineQueue
def perform(build_id)
Ci::Build.find_by(id: build_id)&.update_coverage
......
class BuildFinishedWorker
include Sidekiq::Worker
include BuildQueue
include PipelineQueue
enqueue_in group: :processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
......
class BuildHooksWorker
include Sidekiq::Worker
include BuildQueue
include PipelineQueue
enqueue_in group: :hooks
def perform(build_id)
Ci::Build.find_by(id: build_id)
......
class BuildQueueWorker
include Sidekiq::Worker
include BuildQueue
include PipelineQueue
enqueue_in group: :processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
......
class BuildSuccessWorker
include Sidekiq::Worker
include BuildQueue
include PipelineQueue
enqueue_in group: :processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
......
# Concern for setting Sidekiq settings for the various CI build workers.
module BuildQueue
extend ActiveSupport::Concern
included do
sidekiq_options queue: :build
end
end
##
# Concern for setting Sidekiq settings for the various CI pipeline workers.
#
module PipelineQueue
extend ActiveSupport::Concern
included do
sidekiq_options queue: :pipeline
sidekiq_options queue: 'pipeline_default'
end
class_methods do
def enqueue_in(group:)
raise ArgumentError, 'Unspecified queue group!' if group.empty?
sidekiq_options queue: "pipeline_#{group}"
end
end
end
class ExpireJobCacheWorker
include Sidekiq::Worker
include BuildQueue
include PipelineQueue
enqueue_in group: :cache
def perform(job_id)
job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id)
......
......@@ -2,6 +2,8 @@ class ExpirePipelineCacheWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :cache
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
......
......@@ -7,8 +7,6 @@ class MergeWorker
current_user = User.find(current_user_id)
merge_request = MergeRequest.find(merge_request_id)
merge_request.update_column(:merge_jid, jid)
MergeRequests::MergeService.new(merge_request.target_project, current_user, params)
.execute(merge_request)
end
......
......@@ -2,6 +2,8 @@ class PipelineHooksWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :hooks
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:execute_hooks)
......
......@@ -2,6 +2,8 @@ class PipelineProcessWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:process!)
......
......@@ -2,6 +2,8 @@ class PipelineSuccessWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
MergeRequests::MergeWhenPipelineSucceedsService
......
......@@ -2,6 +2,8 @@ class PipelineUpdateWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:update_status)
......
......@@ -2,6 +2,8 @@ class StageUpdateWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :processing
def perform(stage_id)
Ci::Stage.find_by(id: stage_id).try do |stage|
stage.update_status
......
---
title: Prevents rendering empty badges when request fails
merge_request:
author:
type: fixed
---
title: Remove `is_` prefix from predicate method names
merge_request: 13810
author: Maxim Rydkin
type: other
---
title: Instrument MergeRequest#ensure_ref_fetched
merge_request:
author:
type: other
---
title: Present enqueued merge jobs as Merging as well
merge_request:
author:
---
title: Update and fix resolvable note icons for easier recognition
merge_request:
author:
type: changed
---
title: Never wait for sidekiq jobs when creating projects
merge_request: 13775
author:
type: other
---
title: Disable GitLab Project Import Button if source disabled
merge_request:
author:
type: fixed
---
title: Fix incorrect date/time formatting on prometheus graphs
merge_request: 13865
author:
type: fixed
---
title: Fixed: Notifications weren't sending to participating users with a `Custom` notification setting.
merge_request: 13680
author: jneen
type: fixed
---
title: Fixed diff changes bar buttons from showing/hiding whilst scrolling
merge_request:
author:
type: fixed
---
title: Update 'Using Docker images' documentation
merge_request: 13848
author:
type: other
---
title: Fix events error importing GitLab projects
merge_request:
author:
type: fixed
---
title: Fix pipeline trigger via API fails with 500 Internal Server Error in 9.5
merge_request:
author:
type: fixed
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.
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.
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