Commit d2f70576 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee' into 'master'

CE upstream: Thursday

See merge request !1293
parents 6cbf3079 75881efc
......@@ -51,3 +51,4 @@ eslint-report.html
/builds/*
/shared/*
/.gitlab_workhorse_secret
/webpack-report/
......@@ -224,6 +224,25 @@ rake db:seed_fu:
paths:
- log/development.log
rake gitlab:assets:compile:
stage: test
<<: *dedicated-runner
dependencies: []
variables:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "false"
USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
script:
- bundle exec rake yarn:install gitlab:assets:compile
artifacts:
name: webpack-report
expire_in: 31d
paths:
- webpack-report/
rake karma:
cache:
paths:
......@@ -265,7 +284,7 @@ bundler:audit:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
- "bundle exec bundle-audit check --update --ignore OSVDB-115941 CVE-2016-6316 CVE-2016-6317"
migration paths:
stage: test
......@@ -372,6 +391,7 @@ pages:
dependencies:
- coverage
- rake karma
- rake gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
......@@ -379,6 +399,7 @@ pages:
- mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true
- mv webpack-report/ public/webpack-report/ || true
artifacts:
paths:
- public
......
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
# on 2017-02-22 15:19:47 -0600 using RuboCop version 0.47.1.
# on 2017-02-22 13:02:35 -0600 using RuboCop version 0.47.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
......@@ -43,7 +43,7 @@ RSpec/ScatteredSetup:
RSpec/SingleArgumentMessageChain:
Enabled: false
# Offense count: 169
# Offense count: 172
Rails/FilePath:
Enabled: false
......@@ -64,14 +64,14 @@ Rails/SkipsModelValidations:
Security/YAMLLoad:
Enabled: false
# Offense count: 55
# Offense count: 48
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
# Offense count: 1304
# Offense count: 1336
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
......@@ -102,7 +102,7 @@ Style/EmptyLiteral:
Style/EmptyMethod:
Enabled: false
# Offense count: 201
# Offense count: 203
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
......@@ -114,7 +114,7 @@ Style/ExtraSpacing:
Style/FormatString:
Enabled: false
# Offense count: 316
# Offense count: 318
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
......@@ -123,7 +123,7 @@ Style/GuardClause:
Style/IfInsideElse:
Enabled: false
# Offense count: 193
# Offense count: 192
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
......@@ -181,7 +181,7 @@ Style/NestedParenthesizedCalls:
Style/Next:
Enabled: false
# Offense count: 30
# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
......@@ -200,7 +200,7 @@ Style/NumericPredicate:
Style/ParallelAssignment:
Enabled: false
# Offense count: 486
# Offense count: 488
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
......@@ -259,7 +259,7 @@ Style/RedundantReturn:
Style/RedundantSelf:
Enabled: false
# Offense count: 110
# Offense count: 111
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
......@@ -282,7 +282,7 @@ Style/SelfAssignment:
Style/SingleLineMethods:
Enabled: false
# Offense count: 180
# Offense count: 183
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
......@@ -302,7 +302,7 @@ Style/SpaceBeforeFirstArg:
Style/SpaceInLambdaLiteral:
Enabled: false
# Offense count: 217
# Offense count: 215
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
......@@ -310,7 +310,7 @@ Style/SpaceInLambdaLiteral:
Style/SpaceInsideBlockBraces:
Enabled: false
# Offense count: 100
# Offense count: 101
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
......@@ -334,7 +334,7 @@ Style/SpecialGlobalVars:
Style/StringLiteralsInInterpolation:
Enabled: false
# Offense count: 61
# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
......@@ -355,13 +355,13 @@ Style/TernaryParentheses:
Style/TrailingCommaInArguments:
Enabled: false
# Offense count: 13
# Offense count: 12
# Cop supports --auto-correct.
# Configuration parameters: AllowNamedUnderscoreVariables.
Style/TrailingUnderscoreVariable:
Enabled: false
# Offense count: 89
# Offense count: 83
# Cop supports --auto-correct.
Style/TrailingWhitespace:
Enabled: false
......
......@@ -209,7 +209,7 @@ gem 'babosa', '~> 1.0.2'
gem 'loofah', '~> 2.0.3'
# Working with license
gem 'licensee', '~> 8.0.0'
gem 'licensee', '~> 8.7.0'
# Protect against bruteforcing
gem 'rack-attack', '~> 4.4.1'
......
......@@ -422,8 +422,8 @@ GEM
rubyzip
thor
xml-simple
licensee (8.0.0)
rugged (>= 0.24b)
licensee (8.7.0)
rugged (~> 0.24)
little-plugger (1.1.4)
logging (2.1.0)
little-plugger (~> 1.1)
......@@ -937,7 +937,7 @@ DEPENDENCIES
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
licensee (~> 8.7.0)
loofah (~> 2.0.3)
mail_room (~> 0.9.1)
method_source (~> 0.8)
......
8.17.0-ee-pre
8.18.0-ee-pre
<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg>
class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
$elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
$elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
}
static ajaxBeforeSend(e) {
e.target.setAttribute('disabled', '');
const iconElement = e.target.querySelector('i');
// get first fa- icon
const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first();
iconElement.dataset.icon = originalIcon;
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
}
static ajaxComplete(e) {
e.target.removeAttribute('disabled');
const iconElement = e.target.querySelector('i');
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
}
static toggleLoadingIcon(iconElement) {
const classList = iconElement.classList;
classList.toggle(iconElement.dataset.icon);
classList.toggle('fa-spinner');
classList.toggle('fa-spin');
}
}
window.gl = window.gl || {};
gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global Vue */
/* global BoardService */
function requireAll(context) { return context.keys().map(context); }
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/));
require('./models/issue');
require('./models/label');
require('./models/list');
require('./models/milestone');
require('./models/user');
require('./stores/boards_store');
require('./stores/modal_store');
require('./services/board_service');
require('./mixins/modal_mixins');
require('./mixins/sortable_default_options');
require('./filters/due_date_filters');
require('./components/board');
require('./components/boards_selector');
require('./components/board_sidebar');
......@@ -94,17 +98,53 @@ $(() => {
modal: ModalStore.store,
store: Store.state,
},
watch: {
disabled() {
this.updateTooltip();
},
},
computed: {
disabled() {
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
},
tooltipTitle() {
if (this.disabled) {
return 'Please add a list to your board first';
}
return '';
},
},
methods: {
updateTooltip() {
const $tooltip = $(this.$el);
this.$nextTick(() => {
if (this.disabled) {
$tooltip.tooltip();
} else {
$tooltip.tooltip('destroy');
}
});
},
openModal() {
if (!this.disabled) {
this.toggleModal(true);
}
},
},
mounted() {
this.updateTooltip();
},
template: `
<button
class="btn btn-create pull-right prepend-left-10 has-tooltip"
class="btn btn-create pull-right prepend-left-10"
type="button"
:disabled="disabled"
@click="toggleModal(true)">
data-placement="bottom"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
@click="openModal">
Add issues
</button>
`,
......
......@@ -4,10 +4,20 @@
window.Vue = require('vue');
window.Cookies = require('js-cookie');
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/));
require('./svg/icon_branch');
require('./svg/icon_build_status');
require('./svg/icon_commit');
require('./components/stage_code_component');
require('./components/stage_issue_component');
require('./components/stage_plan_component');
require('./components/stage_production_component');
require('./components/stage_review_component');
require('./components/stage_staging_component');
require('./components/stage_test_component');
require('./components/total_time_component');
require('./cycle_analytics_service');
require('./cycle_analytics_store');
require('./default_event_objects');
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
......
/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */
/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
/* global Vue */
/* global ResolveCount */
/* global ResolveServiceClass */
function requireAll(context) { return context.keys().map(context); }
const Vue = require('vue');
requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
require('./models/discussion');
require('./models/note');
require('./stores/comments');
require('./services/resolve');
require('./mixins/discussion');
require('./components/comment_resolve_btn');
require('./components/jump_to_discussion');
require('./components/resolve_btn');
require('./components/resolve_count');
require('./components/resolve_discussion_btn');
$(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath;
......
......@@ -110,6 +110,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:compare:show':
new gl.Diff();
break;
case 'projects:branches:index':
gl.AjaxLoadingSpinner.init();
break;
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
......
......@@ -126,13 +126,14 @@ require('./preview_markdown');
};
pasteText = function(text) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text + "\n\n";
caretStart = $(child)[0].selectionStart;
caretEnd = $(child)[0].selectionEnd;
textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + text + afterSelection);
child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length);
$(child).val(beforeSelection + formattedText + afterSelection);
child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
return form_textarea.trigger("input");
};
getFilename = function(e) {
......
......@@ -15,12 +15,12 @@ module.exports = Vue.component('actions-component', {
},
template: `
<div class="inline">
<div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
<div class="btn-group" role="group">
<button class="dropdown btn btn-default dropdown-new" data-toggle="dropdown">
<span>
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i>
</a>
</span>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
......@@ -37,7 +37,7 @@ module.exports = Vue.component('actions-component', {
</a>
</li>
</ul>
</div>
</button>
</div>
`,
});
......@@ -505,39 +505,26 @@ module.exports = Vue.component('environment-item', {
<td class="hidden-xs">
<div v-if="!model.isFolder">
<div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container">
<actions-component
<div class="btn-group" role="group">
<actions-component v-if="hasManualActions && canCreateDeployment"
:play-icon-svg="playIconSvg"
:actions="manualActions">
</actions-component>
</div>
<div v-if="externalURL && canReadEnvironment"
class="inline js-external-url-container">
<external-url-component
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL">
</external-url-component>
</div>
<div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path">
</stop-component>
</div>
<div v-if="model && model.terminal_path"
class="inline js-terminal-button-container">
<terminal-button-component
<terminal-button-component v-if="model && model.terminal_path"
:terminal-icon-svg="terminalIconSvg"
:terminal-path="model.terminal_path">
</terminal-button-component>
</div>
<div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container">
<rollback-component
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl">
</rollback-component>
......
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/));
require('./dropdown_hint');
require('./dropdown_non_user');
require('./dropdown_user');
require('./dropdown_utils');
require('./filtered_search_dropdown_manager');
require('./filtered_search_dropdown');
require('./filtered_search_manager');
require('./filtered_search_token_keys');
require('./filtered_search_token_keys_with_weights');
require('./filtered_search_tokenizer');
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/));
require('./stat_graph_contributors_graph');
require('./stat_graph_contributors_util');
require('./stat_graph_contributors');
require('./stat_graph');
......@@ -2,9 +2,8 @@
/* global Network */
/* global ShortcutsNetwork */
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
require('./branch_graph');
require('./network');
(function() {
$(function() {
......
(() => {
class VersionCheckImage {
class VersionCheckImage {
static bindErrorEvent(imageElement) {
imageElement.off('error').on('error', () => imageElement.hide());
}
}
}
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
})();
module.exports = VersionCheckImage;
......@@ -23,7 +23,7 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s
apiScope: 'all',
pageInfo: {},
pagenum: 1,
count: { all: 0, running_or_pending: 0 },
count: {},
pageRequest: false,
};
},
......
......@@ -148,11 +148,16 @@ header {
}
.header-logo {
display: inline-block;
margin: 0 8px 0 3px;
position: relative;
position: absolute;
left: 50%;
top: 7px;
transition-duration: .3s;
z-index: 999;
#logo {
position: relative;
left: -50%;
}
svg,
img {
......@@ -162,6 +167,15 @@ header {
&:hover {
cursor: pointer;
}
@media (max-width: $screen-xs-max) {
right: 20px;
left: auto;
#logo {
left: auto;
}
}
}
.title {
......@@ -169,7 +183,7 @@ header {
padding-right: 20px;
margin: 0;
font-size: 18px;
max-width: 450px;
max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
......@@ -179,6 +193,10 @@ header {
vertical-align: top;
white-space: nowrap;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
}
@media (max-width: $screen-xs-max) {
max-width: 190px;
}
......
......@@ -96,16 +96,6 @@ ul.unstyled-list > li {
border-bottom: none;
}
ul.task-list {
li.task-list-item {
list-style-type: none;
}
ul:not(.task-list) {
padding-left: 1.3em;
}
}
// Generic content list
ul.content-list {
@include basic-list;
......
......@@ -76,6 +76,13 @@
#{$property}: $value;
}
/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */
@mixin on-webkit-only {
@media screen and (-webkit-min-device-pixel-ratio:0) {
@content;
}
}
@mixin keyframes($animation-name) {
@-webkit-keyframes #{$animation-name} {
@content;
......
@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
z-index: 1;
z-index: 2;
position: absolute;
bottom: 12px;
width: 43px;
......@@ -18,7 +18,7 @@
.fa {
position: relative;
top: 6px;
top: 5px;
font-size: 18px;
}
}
......@@ -79,6 +79,7 @@
}
&.sub-nav {
text-align: center;
background-color: $gray-normal;
.container-fluid {
......@@ -286,6 +287,7 @@
background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
.container-fluid {
position: relative;
......@@ -351,7 +353,7 @@
right: -5px;
.fa {
right: -28px;
right: -7px;
}
}
......@@ -381,7 +383,7 @@
left: 0;
.fa {
left: -4px;
left: 10px;
}
}
}
......
......@@ -29,14 +29,16 @@
}
}
.right-sidebar-collapsed {
padding-right: 0;
@media (min-width: $screen-sm-min) {
@media (min-width: $screen-sm-min) {
.content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
.right-sidebar-collapsed {
padding-right: 0;
@media (min-width: $screen-sm-min) {
.merge-request-tabs-holder.affix {
right: $gutter_collapsed_width;
}
......@@ -54,12 +56,6 @@
.right-sidebar-expanded {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $gutter_collapsed_width;
}
}
@media (min-width: $screen-md-min) {
.content-wrapper {
padding-right: $gutter_width;
......
......@@ -134,7 +134,7 @@
ul,
ol {
padding: 0;
margin: 3px 0 3px 28px !important;
margin: 3px 0 !important;
}
ul:dir(rtl),
......@@ -144,6 +144,29 @@
li {
line-height: 1.6em;
margin-left: 25px;
padding-left: 3px;
/* Normalize the bullet position on webkit. */
@include on-webkit-only {
margin-left: 28px;
padding-left: 0;
}
}
ul.task-list {
li.task-list-item {
list-style-type: none;
position: relative;
padding-left: 28px;
margin-left: 0 !important;
input.task-list-item-checkbox {
position: absolute;
left: 8px;
top: 5px;
}
}
}
a[href*="/uploads/"],
......
......@@ -35,7 +35,6 @@
display: table-cell;
}
.environments-name,
.environments-commit,
.environments-actions {
width: 20%;
......@@ -45,6 +44,7 @@
width: 10%;
}
.environments-name,
.environments-deploy,
.environments-build {
width: 15%;
......@@ -62,6 +62,22 @@
}
}
.btn-group {
> a {
color: $gl-text-color-secondary;
}
svg path {
fill: $gl-text-color-secondary;
}
.dropdown {
outline: none;
}
}
.commit-title {
margin: 0;
}
......
......@@ -10,6 +10,11 @@
.issue-labels {
display: inline-block;
}
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
}
}
}
......
......@@ -652,24 +652,30 @@ pre.light-well {
}
}
.container-fluid.project-stats-container {
@media (max-width: $screen-xs-max) {
padding: 12px 0;
.project-last-commit {
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
}
}
.project-last-commit {
&.container-fluid {
padding-top: 12px;
padding-bottom: 12px;
background-color: $gray-light;
padding: 12px $gl-padding;
border: 1px solid $border-color;
border-right-width: 0;
border-left-width: 0;
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
border-right-width: 1px;
border-left-width: 1px;
}
}
@media (min-width: $screen-sm-min) {
&.container-limited {
@media (min-width: 1281px) {
border-radius: $border-radius-base;
}
}
.ci-status {
margin-right: $gl-padding;
......
......@@ -9,7 +9,7 @@ module IssuableCollections
private
def issuable_meta_data(issuable_collection)
def issuable_meta_data(issuable_collection, collection_type)
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
......@@ -17,16 +17,24 @@ module IssuableCollections
issuable_ids = issuable_collection.map(&:id)
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_merge_requests_count =
if collection_type == 'Issue'
MergeRequestsClosingIssues.count_for_collection(issuable_ids)
else
[]
end
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
notes = issuable_note_count.find { |notes| notes.noteable_id == id }
merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id }
issuable_meta[id] = Issuable::IssuableMeta.new(
upvotes.try(:count).to_i,
downvotes.try(:count).to_i,
notes.try(:count).to_i
notes.try(:count).to_i,
merge_requests.try(:last).to_i
)
end
end
......
......@@ -10,7 +10,7 @@ module IssuesAction
.page(params[:page])
@collection_type = "Issue"
@issuable_meta_data = issuable_meta_data(@issues)
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
respond_to do |format|
format.html
......
......@@ -9,7 +9,7 @@ module MergeRequestsAction
.page(params[:page])
@collection_type = "MergeRequest"
@issuable_meta_data = issuable_meta_data(@merge_requests)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
end
private
......
......@@ -17,13 +17,31 @@ module SpammableActions
private
def recaptcha_params
return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
def recaptcha_check_with_fallback(&fallback)
if spammable.valid?
redirect_to spammable
elsif render_recaptcha?
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
render :verify
else
yield
end
end
def spammable_params
default_params = { request: request }
recaptcha_check = params[:recaptcha_verification] &&
Gitlab::Recaptcha.load_configurations! &&
verify_recaptcha
return default_params unless recaptcha_check
{
recaptcha_verified: true,
spam_log_id: params[:spam_log_id]
}
{ recaptcha_verified: true,
spam_log_id: params[:spam_log_id] }.merge(default_params)
end
def spammable
......
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
before_action :find_todos, only: [:index, :destroy_all]
def index
......@@ -48,8 +50,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def todos_counts
{
count: current_user.todos_pending_count,
done_count: current_user.todos_done_count
count: number_with_delimiter(current_user.todos_pending_count),
done_count: number_with_delimiter(current_user.todos_done_count)
}
end
end
......@@ -26,7 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController
@collection_type = "Issue"
@issues = issues_collection
@issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues)
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
......@@ -99,15 +99,15 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
extra_params.merge!(recaptcha_params)
create_params = issue_params
.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
.merge(spammable_params)
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
@issue = Issues::CreateService.new(project, current_user, create_params).execute
respond_to do |format|
format.html do
html_response_create
recaptcha_check_with_fallback { render :new }
end
format.js do
@link = @issue.attachment.url.to_js
......@@ -116,7 +116,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
def update
@issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
update_params = issue_params.merge(spammable_params)
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id])
......@@ -128,11 +130,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html do
if @issue.valid?
redirect_to issue_path(@issue)
else
render :edit
end
recaptcha_check_with_fallback { render :edit }
end
format.json do
......@@ -184,20 +182,6 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def html_response_create
if @issue.valid?
redirect_to issue_path(@issue)
elsif render_recaptcha?
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
render :verify
else
render :new
end
end
def issue
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
......
......@@ -42,7 +42,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
@issuable_meta_data = issuable_meta_data(@merge_requests)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
......
......@@ -13,9 +13,15 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
@running_or_pending_count = PipelinesFinder
@running_count = PipelinesFinder
.new(project).execute(scope: 'running').count
@pending_count = PipelinesFinder
.new(project).execute(scope: 'pending').count
@finished_count = PipelinesFinder
.new(project).execute(scope: 'finished').count
@pipelines_count = PipelinesFinder
.new(project).execute.count
......@@ -29,7 +35,9 @@ class Projects::PipelinesController < Projects::ApplicationController
.represent(@pipelines),
count: {
all: @pipelines_count,
running_or_pending: @running_or_pending_count
running: @running_count,
pending: @pending_count,
finished: @finished_count,
}
}
end
......
......@@ -38,24 +38,19 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
create_params = snippet_params.merge(request: request)
create_params = snippet_params.merge(spammable_params)
@snippet = CreateSnippetService.new(@project, current_user, create_params).execute
if @snippet.valid?
respond_with(@snippet,
location: namespace_project_snippet_path(@project.namespace,
@project, @snippet))
else
render :new
end
recaptcha_check_with_fallback { render :new }
end
def update
UpdateSnippetService.new(project, current_user, @snippet,
snippet_params).execute
respond_with(@snippet,
location: namespace_project_snippet_path(@project.namespace,
@project, @snippet))
update_params = snippet_params.merge(spammable_params)
UpdateSnippetService.new(project, current_user, @snippet, update_params).execute
recaptcha_check_with_fallback { render :edit }
end
def show
......
......@@ -43,16 +43,19 @@ class SnippetsController < ApplicationController
end
def create
create_params = snippet_params.merge(request: request)
create_params = snippet_params.merge(spammable_params)
@snippet = CreateSnippetService.new(nil, current_user, create_params).execute
respond_with @snippet.becomes(Snippet)
recaptcha_check_with_fallback { render :new }
end
def update
UpdateSnippetService.new(nil, current_user, @snippet,
snippet_params).execute
respond_with @snippet.becomes(Snippet)
update_params = snippet_params.merge(spammable_params)
UpdateSnippetService.new(nil, current_user, @snippet, update_params).execute
recaptcha_check_with_fallback { render :edit }
end
def show
......
......@@ -10,7 +10,11 @@ class PipelinesFinder
scoped_pipelines =
case scope
when 'running'
pipelines.running_or_pending
pipelines.running
when 'pending'
pipelines.pending
when 'finished'
pipelines.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
......
module EmailsHelper
include AppearancesHelper
# Google Actions
# https://developers.google.com/gmail/markup/reference/go-to-action
def email_action(url)
......@@ -49,4 +51,19 @@ module EmailsHelper
msg = "This link is valid for #{password_reset_token_valid_time}. "
msg << "After it expires, you can #{link_tag}."
end
def header_logo
if brand_item && brand_item.header_logo?
image_tag(
brand_item.header_logo,
style: 'height: 50px'
)
else
image_tag(
image_url('mailers/gitlab_header_logo.gif'),
size: "55x50",
alt: "GitLab"
)
end
end
end
......@@ -22,8 +22,8 @@ module Emails
mail(bcc: recipients,
subject: pipeline_subject(status),
skip_premailer: true) do |format|
format.html { render layout: false }
format.text
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end
......
......@@ -95,8 +95,11 @@ module Ci
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
relation = ref ? where(ref: ref) : self
relation.where(id: max_id)
if ref
where(ref: ref, id: max_id.where(ref: ref))
else
where(id: max_id)
end
end
def self.latest_status(ref = nil)
......
......@@ -16,9 +16,9 @@ module Issuable
include TimeTrackable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes and notes count for issues and merge requests
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count)
included do
cache_markdown_field :title, pipeline: :single_line
......
......@@ -13,7 +13,7 @@ module Spammable
attr_accessor :spam
attr_accessor :spam_log
after_validation :check_for_spam, on: :create
after_validation :check_for_spam, on: [:create, :update]
cattr_accessor :spammable_attrs, instance_accessor: false do
[]
......
......@@ -210,7 +210,11 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
opts = diff_options || {}
# The `#diffs` method ends up at an instance of a class inheriting from
# `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
# here too, to get the same diff size without performing highlighting.
#
opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
raw_diffs(opts).size
end
......
......@@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
validates :issue_id, presence: true
class << self
def count_for_collection(ids)
group(:issue_id).
where(issue_id: ids).
pluck('issue_id', 'COUNT(*) as count')
end
end
end
......@@ -14,8 +14,4 @@ class ProjectSnippet < Snippet
participant :author
participant :notes_with_associations
def check_for_spam?
super && project.public?
end
end
......@@ -63,7 +63,8 @@ module Ci
private
def skip_ci?
pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def commit
......
module Ci
class RetryBuildService < ::BaseService
CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name
CLONE_ATTRIBUTES = %i[pipeline project ref tag options commands name
allow_failure stage stage_idx trigger_request
yaml_variables when environment coverage_regex]
.freeze
REJECT_ATTRIBUTES = %i[id status user token coverage trace runner
artifacts_file artifacts_metadata artifacts_size
artifacts_expire_at artifacts_file
artifacts_metadata artifacts_size
created_at updated_at started_at finished_at
queued_at erased_by erased_at].freeze
IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url
IGNORE_ATTRIBUTES = %i[type lock_version gl_project_id target_url
deploy job_id description].freeze
def execute(build)
......
module Ci
class RetryPipelineService < ::BaseService
include Gitlab::OptimisticLocking
def execute(pipeline)
unless can?(current_user, :update_pipeline, pipeline)
raise Gitlab::Access::AccessDeniedError
......@@ -12,6 +14,10 @@ module Ci
.reprocess(build)
end
pipeline.builds.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process }
end
MergeRequests::AddTodoWhenBuildFailsService
.new(project, current_user)
.close_all(pipeline)
......
class CreateSnippetService < BaseService
include SpamCheckService
def execute
request = params.delete(:request)
api = params.delete(:api)
filter_spam_check_params
snippet = if project
project.snippets.build(params)
......@@ -15,10 +16,11 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
snippet.spam = SpamService.new(snippet, request).check(api)
spam_check(snippet, current_user)
if snippet.save
UserAgentDetailService.new(snippet, request).create
UserAgentDetailService.new(snippet, @request).create
end
snippet
......
......@@ -191,14 +191,12 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
def after_update(issuable)
def before_update(issuable)
# To be overridden by subclasses
end
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user))
end
def after_update(issuable)
# To be overridden by subclasses
end
def update(issuable)
......@@ -212,7 +210,12 @@ class IssuableBaseService < BaseService
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present? && update_issuable(issuable, params)
if params.present?
issuable.assign_attributes(params.merge(updated_by: current_user))
before_update(issuable)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels)
......@@ -223,6 +226,7 @@ class IssuableBaseService < BaseService
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
end
issuable
end
......
module Issues
class CreateService < Issues::BaseService
include SpamCheckService
def execute
@request = params.delete(:request)
@api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
filter_spam_check_params
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute
......@@ -12,14 +11,8 @@ module Issues
create(@issue)
end
def before_create(issuable)
if @recaptcha_verified
spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
spam_log&.update!(recaptcha_verified: true)
else
issuable.spam = spam_service.check(@api)
issuable.spam_log = spam_service.spam_log
end
def before_create(issue)
spam_check(issue, current_user)
end
def after_create(issuable)
......@@ -42,10 +35,6 @@ module Issues
private
def spam_service
@spam_service ||= SpamService.new(@issue, @request)
end
def user_agent_detail_service
UserAgentDetailService.new(@issue, @request)
end
......
module Issues
class UpdateService < Issues::BaseService
include SpamCheckService
def execute(issue)
filter_spam_check_params
update(issue)
end
def before_update(issue)
spam_check(issue, current_user)
end
def handle_changes(issue, old_labels: [], old_mentioned_users: [])
if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
......
......@@ -2,18 +2,14 @@ module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
self.merge_request = MergeRequest.new(params)
merge_request.can_be_created = true
merge_request.compare_commits = []
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified?
if branches_specified? && branches_valid?
compare_branches
assign_title_and_description
else
merge_request.can_be_created = false
end
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
merge_request
end
......@@ -37,11 +33,17 @@ module MergeRequests
target_branch || target_project.default_branch
end
def branches_specified?
params[:source_branch] && params[:target_branch]
def source_branch_specified?
params[:source_branch].present?
end
def target_branch_specified?
params[:target_branch].present?
end
def branches_valid?
return false unless source_branch_specified? || target_branch_specified?
validate_branches
errors.blank?
end
......@@ -55,9 +57,11 @@ module MergeRequests
target_branch
)
if compare
merge_request.compare_commits = compare.commits
merge_request.compare = compare
end
end
def validate_branches
add_error('You must select source and target branch') unless branches_present?
......
# SpamCheckService
#
# Provide helper methods for checking if a given spammable object has
# potential spam data.
#
# Dependencies:
# - params with :request
#
module SpamCheckService
def filter_spam_check_params
@request = params.delete(:request)
@api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
end
def spam_check(spammable, user)
spam_service = SpamService.new(spammable, @request)
spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do
user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
end
end
end
......@@ -17,15 +17,6 @@ class SpamService
end
end
def check(api = false)
return false unless request && check_for_spam?
return false unless akismet.is_spam?
create_spam_log(api)
true
end
def mark_as_spam!
return false unless spammable.submittable_as_spam?
......@@ -36,8 +27,30 @@ class SpamService
end
end
def when_recaptcha_verified(recaptcha_verified, api = false)
# In case it's a request which is already verified through recaptcha, yield
# block.
if recaptcha_verified
yield
else
# Otherwise, it goes to Akismet and check if it's a spam. If that's the
# case, it assigns spammable record as "spam" and create a SpamLog record.
spammable.spam = check(api)
spammable.spam_log = spam_log
end
end
private
def check(api)
return false unless request && check_for_spam?
return false unless akismet.is_spam?
create_spam_log(api)
true
end
def akismet
@akismet ||= AkismetService.new(
spammable_owner,
......
class UpdateSnippetService < BaseService
include SpamCheckService
attr_accessor :snippet
def initialize(project, user, snippet, params)
......@@ -17,6 +19,10 @@ class UpdateSnippetService < BaseService
end
end
snippet.update_attributes(params)
filter_spam_check_params
snippet.assign_attributes(params)
spam_check(snippet, current_user)
snippet.save
end
end
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: container_class }
= nav_link(path: 'groups#show', html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Group Home' do
%span
Home
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: container_class }
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do
%span
List
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: 'Labels' do
%span
Labels
= nav_link(path: 'milestones#index') do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span
Milestones
......@@ -3,6 +3,7 @@
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
- page_title "Activity"
= render 'groups/head'
%section.activities
= render 'activities'
- page_title "Members"
= render 'groups/head'
.project-members-page.prepend-top-default
%h4
......
- page_title "Issues"
= render "head_issues"
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
......
- page_title 'Labels'
= render "groups/head_issues"
.top-area.adjust
.nav-text
......
- page_title "Milestones"
= render "groups/head_issues"
.top-area
= render 'shared/milestones_filter'
......
......@@ -4,6 +4,7 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
= render 'groups/head'
= render 'groups/home_panel'
......
.page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav
.layout-nav
%div{ class: container_class }
.container-fluid
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
......
- humanized_resource_name = spammable.class.model_name.human.downcase
- resource_name = spammable.class.model_name.singular
%h3.page-title
Anti-spam verification
%hr
%p
#{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
= form_for form do |f|
.recaptcha
- params[resource_name].each do |field, value|
= hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags
-# Yields a block with given extra params.
= yield
.row-content-block.footer-block
= f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
......@@ -67,12 +67,12 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
%h1.title= title
= yield :header_content
= render 'shared/outdated_browser'
......
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* ANDROID MARGIN HACK */
body { margin:0 !important; }
div[style*="margin: 16px 0"] { margin:0 !important; }
@media only screen and (max-width: 639px) {
body, #body {
min-width: 320px !important;
}
table.wrapper {
width: 100% !important;
min-width: 320px !important;
}
table.wrapper > tbody > tr > td {
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
}
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
= header_logo
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
= yield
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
%a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
%a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
%a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
= yield
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
Manage all notifications: #{profile_notifications_url}
Help: #{help_url}
......@@ -5,23 +5,11 @@
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= nav_link(path: ['groups#show', 'groups#activity', 'group_members#index'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do
%span
Group
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
= nav_link(controller: [:group, :labels]) do
= link_to group_labels_path(@group), title: 'Labels' do
%span
Labels
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span
Milestones
= nav_link(path: 'groups#issues') do
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
%span
Issues
......@@ -33,10 +21,6 @@
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
= nav_link(controller: [:stats]) do
= link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do
%span
......
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* ANDROID MARGIN HACK */
body { margin:0 !important; }
div[style*="margin: 16px 0"] { margin:0 !important; }
@media only screen and (max-width: 639px) {
body, #body {
min-width: 320px !important;
}
table.wrapper {
width: 100% !important;
min-width: 320px !important;
}
table.wrapper > tbody > tr > td {
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
}
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.alert
%tr.alert
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
......@@ -66,10 +7,10 @@
%img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has failed.
%tr.spacer
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
......@@ -127,11 +68,11 @@
- else
%span
= commit.author_name
%tr.spacer
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
- failed = @pipeline.statuses.latest.failed
%tr.pre-section
- failed = @pipeline.statuses.latest.failed
%tr.pre-section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
Pipeline
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
......@@ -140,10 +81,10 @@
= failed.size
failed
#{'build'.pluralize(failed.size)}.
%tr.warning
%tr.warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
%table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
%tbody
......@@ -166,14 +107,3 @@
= build.trace_html(last_lines: 10).html_safe
- else
%td{ colspan: "2" }
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
%a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
%a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
%a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
......@@ -27,7 +27,3 @@ Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
<% end -%>
<% end -%>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
Manage all notifications: <%= profile_notifications_url %>
Help: <%= help_url %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* ANDROID MARGIN HACK */
body { margin:0 !important; }
div[style*="margin: 16px 0"] { margin:0 !important; }
@media only screen and (max-width: 639px) {
body, #body {
min-width: 320px !important;
}
table.wrapper {
width: 100% !important;
min-width: 320px !important;
}
table.wrapper > tbody > tr > td {
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
}
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.success
%tr.success
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
......@@ -66,10 +7,10 @@
%img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has passed.
%tr.spacer
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
......@@ -127,10 +68,10 @@
- else
%span
= commit.author_name
%tr.spacer
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.success-message
%tr.success-message
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
- build_count = @pipeline.statuses.latest.size
- stage_count = @pipeline.stages_count
......@@ -141,14 +82,3 @@
#{build_count} #{'build'.pluralize(build_count)}
in
#{stage_count} #{'stage'.pluralize(stage_count)}.
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
%a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
%a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
%a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
......@@ -18,7 +18,3 @@ Commit Author: <%= commit.author_name %>
<% build_count = @pipeline.statuses.latest.size -%>
<% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
Manage all notifications: <%= profile_notifications_url %>
Help: <%= help_url %>
......@@ -37,7 +37,7 @@
- if can?(current_user, :push_code, @project)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name),
class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
class: "btn btn-remove remove-row js-ajax-loading-spinner #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
method: :delete,
data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
remote: true,
......
- page_title "Anti-spam verification"
- form = [@project.namespace.becomes(Namespace), @project, @issue]
%h3.page-title
Anti-spam verification
%hr
%p
We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue.
= form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f|
.recaptcha
- params[:issue].each do |field, value|
= hidden_field(:issue, field, value: value)
= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do
= hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
= hidden_field_tag(:spam_log_id, @issue.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags
.row-content-block.footer-block
= f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create'
......@@ -9,23 +9,35 @@
%div{ class: container_class }
.top-area
%ul.nav-links
%li{ class: active_when(@scope.nil?) }>
%li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }>
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
%li{ class: active_when(@scope == 'running') }>
%li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
= link_to project_pipelines_path(@project, scope: :pending) do
Pending
%span.badge
= number_with_delimiter(@pending_count)
%li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
= number_with_delimiter(@running_count)
%li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
= link_to project_pipelines_path(@project, scope: :finished) do
Finished
%span.badge
= number_with_delimiter(@finished_count)
%li{ class: active_when(@scope == 'branches') }>
%li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
%li{ class: active_when(@scope == 'tags') }>
%li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
......
......@@ -25,3 +25,10 @@
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', badge.to_html)
.row
%hr
.row
.col-md-2.text-center
AsciiDoc
.col-md-10.code.js-syntax-highlight
= highlight('.adoc', badge.to_asciidoc)
......@@ -16,8 +16,7 @@
= render "home_panel"
- if current_user && can?(current_user, :download_code, @project)
.project-stats-container{ class: container_class }
%nav.project-stats
%nav.project-stats{ class: container_class }
%ul.nav
%li
= link_to project_files_path(@project) do
......@@ -78,7 +77,7 @@
Set up auto deploy
- if @repository.commit
.project-last-commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
%div{ class: container_class }
......
- form = [@project.namespace.becomes(Namespace), @project, @snippet.becomes(Snippet)]
= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
......@@ -6,5 +6,5 @@
= f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
.form-group
= f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
= f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
= f.submit btn_text, class: "btn btn-save"
......@@ -2,6 +2,12 @@
- issue_votes = @issuable_meta_data[issuable.id]
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
%li
= image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged')
= issuable_mr
- if upvotes > 0
%li
......
- form = [@snippet.becomes(Snippet)]
= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
---
title: Align task list checkboxes
merge_request: 6487
author: Jared Deckard <jared.deckard@gmail.com>
---
title: Added AsciiDoc Snippet to CI/CD Badges
merge_request: 9164
author: Jan Christophersen
---
title: Clean-up Groups navigation order
merge_request: 9309
author:
---
title: Add all available statuses to scope filter for project builds endpoint
merge_request:
author: George Andrinopoulos
---
title: Add `copy` backup strategy to combat file changed errors
merge_request: 8728
author:
---
title: Adds Pending and Finished tabs to pipelines page
merge_request:
author:
---
title: Add housekeeping endpoint for Projects API
merge_request: 9421
author:
---
title: Spam check and reCAPTCHA improvements
merge_request:
author:
---
title: test compiling production assets and generate webpack bundle report in CI
merge_request: 9396
author:
---
title: Fixes delimiter removes when todo marked as done
merge_request: 9435
author:
---
title: Document when current coverage configuration option was introduced
merge_request: 9443
author:
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.
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