Commit 00ca7adc authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into fix/rename-mwbs-to-merge-when-pipeline-succeeds

* master: (110 commits)
  Rewrite an HTTP link to use HTTPS
  Edit /spec/features/profiles/preferences_spec.rb to match changes in 084d90ac
  Add blue back to sub nav active
  Remove JSX/React eslint plugins.
  Fix a transient spec failure
  Adds hoverstates for collapsed Issue/Merge Request sidebar
  Moved groups above projects
  Add StackProf to the Gemfile, along with a utility to get a profile for a spec
  Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1
  Add a CHANGELOG entry
  Alert user when logged in user email is not the same as the invitation
  Expose timestamp in build entity used by serializer
  Rename `MergeRequest#pipeline` to `head_pipeline`
  Remove unnecessary database indexes
  CE-specific changes gitlab-org/gitlab-ee#1137
  Fixing typo & Clarifying Key name
  fix started_at check
  fix blob controller spec failure - updated not to use file-path-
  fix blob controller spec failure
  Merge branch 'jej-use-issuable-finder-instead-of-access-check' into 'security'
  ...

Conflicts:
	app/controllers/projects/merge_requests_controller.rb
	lib/api/merge_requests.rb
	spec/requests/api/merge_requests_spec.rb
parents adb3f3d4 7e5fa10b
/coverage/
/coverage-javascript/
/public/
/tmp/
......
{
"env": {
"jquery": true,
"browser": true,
"es6": true
},
"extends": "airbnb",
"extends": "airbnb-base",
"globals": {
"$": false,
"_": false,
"gl": false,
"gon": false,
"jQuery": false
"gon": false
},
"plugins": [
"filenames"
......
......@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.14.1 (2016-11-28)
- Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps)
- Update grape entity to 0.6.0. !7491
- If Build running change accept merge request when build succeeds button from orange to blue. !7577
- Changed import sources buttons to checkboxes. !7598 (Luke "Jared" Bennett)
- Last minute CI Style tweaks for 8.14. !7643
- Fix exceptions when loading build trace. !7658
- Fix wrong template rendered when CI/CD settings aren't update successfully. !7665
- fixes last_deployment call environment is nil. !7671
- Sort builds by name within pipeline graph. !7681
- Correctly determine mergeability of MR with no discussions.
- Sidekiq stats in the admin area will now show correctly on different platforms. (blackst0ne)
- Fixed issue boards dragging card removing random issues.
- Fix information disclosure in `Projects::BlobController#update`.
- Fix missing access checks on issue lookup using IssuableFinder.
- Replace issue access checks with use of IssuableFinder.
- Non members cannot create labels through the API.
- Fix cycle analytics plan stage when commits are missing.
## 8.14.0 (2016-11-22)
- Use separate email-token for incoming email and revert back the inactive feature. !5914
......@@ -202,6 +222,15 @@ entry.
- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page
## 8.13.7 (2016-11-28)
- fixes 500 error on project show when user is not logged in and project is still empty. !7376
- Update grape entity to 0.6.0. !7491
- Fix information disclosure in `Projects::BlobController#update`.
- Fix missing access checks on issue lookup using IssuableFinder.
- Replace issue access checks with use of IssuableFinder.
- Non members cannot create labels through the API.
## 8.13.6 (2016-11-17)
- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002
......
......@@ -133,7 +133,7 @@ gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2'
gem 'sidekiq-cron', '~> 0.4.0'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
......@@ -309,6 +309,8 @@ group :development, :test do
gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
gem 'stackprof', '~> 0.2.10'
end
group :test do
......
......@@ -614,7 +614,8 @@ GEM
rubyntlm (0.5.2)
rubypants (0.2.0)
rubyzip (1.2.0)
rufus-scheduler (3.1.10)
rufus-scheduler (3.3.0)
tzinfo
rugged (0.24.0)
safe_yaml (1.0.4)
sanitize (2.1.0)
......@@ -650,10 +651,10 @@ GEM
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (~> 1.5)
redis (~> 3.2, >= 3.2.1)
sidekiq-cron (0.4.0)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0)
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
simplecov (0.12.0)
......@@ -691,6 +692,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
activemodel (>= 4.1, < 5.1)
......@@ -925,7 +927,7 @@ DEPENDENCIES
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
slack-notifier (~> 1.2.0)
......@@ -937,6 +939,7 @@ DEPENDENCIES
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.7.0)
sprockets-es6 (~> 0.9.2)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
teaspoon (~> 1.1.0)
......
# GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
......@@ -84,7 +84,7 @@ For more information please see the [architecture documentation](https://docs.gi
## UX design
Please adhere to the [UX Guide](doc/development/ux_guide/readme.md) when creating designs and implementing code.
Please adhere to the [UX Guide](doc/development/ux_guide/index.md) when creating designs and implementing code.
## Third-party applications
......
......@@ -10,10 +10,15 @@
},
template: `
<span class="total-time">
<template v-if="Object.keys(time).length">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
</template>
<template v-else>
--
</template>
</span>
`,
});
......
......@@ -208,6 +208,9 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
case 'projects:variables:index':
new gl.ProjectVariables();
break;
}
switch (path.first()) {
case 'admin':
......
......@@ -181,7 +181,7 @@
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner spin"></i>
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="blank-state blank-state-no-icon"
......
......@@ -6,3 +6,19 @@ Array.prototype.first = function() {
Array.prototype.last = function() {
return this[this.length-1];
}
Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
if (typeof predicate !== 'function') throw new TypeError('predicate must be a function');
const list = Object(this);
const thisArg = args[1];
let value = {};
for (let i = 0; i < list.length; i += 1) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) return value;
}
return undefined;
};
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */
/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */
(function() {
this.LabelsSelect = (function() {
function LabelsSelect() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */
/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */
/*= require autosave */
/*= require autosize */
......
(() => {
const HIDDEN_VALUE_TEXT = '******';
class ProjectVariables {
constructor() {
this.$revealBtn = $('.js-btn-toggle-reveal-values');
this.$revealBtn.on('click', this.toggleRevealState.bind(this));
}
toggleRevealState(e) {
e.preventDefault();
const oldStatus = this.$revealBtn.attr('data-status');
let newStatus = 'hidden';
let newAction = 'Reveal Values';
if (oldStatus === 'hidden') {
newStatus = 'revealed';
newAction = 'Hide Values';
}
this.$revealBtn.attr('data-status', newStatus);
const $variables = $('.variable-value');
$variables.each((_, variable) => {
const $variable = $(variable);
let newText = HIDDEN_VALUE_TEXT;
if (newStatus === 'revealed') {
newText = $variable.attr('data-value');
}
$variable.text(newText);
});
this.$revealBtn.text(newAction);
}
}
window.gl = window.gl || {};
window.gl.ProjectVariables = ProjectVariables;
})();
......@@ -80,6 +80,7 @@
border-radius: 0;
border: none;
height: auto;
width: 100%;
margin: 0;
align-self: center;
}
......
......@@ -15,7 +15,7 @@
@include btn-default;
}
@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) {
@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) {
background-color: $background;
color: $text;
border-color: $border;
......@@ -23,8 +23,14 @@
&:hover,
&:focus {
background-color: $hover-background;
color: $hover-text;
border-color: $hover-border;
color: $hover-text;
}
&:active {
background-color: $active-background;
border-color: $active-border;
color: $hover-text;
}
}
......@@ -82,11 +88,11 @@
}
@mixin btn-gray {
@include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark);
@include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
}
@mixin btn-white {
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color);
}
@mixin btn-with-margin {
......@@ -139,11 +145,11 @@
&.btn-new,
&.btn-create,
&.btn-save {
@include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light);
@include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal);
}
&.btn-remove {
@include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
@include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
}
}
......@@ -165,11 +171,11 @@
}
&.btn-close {
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
@include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal);
}
&.btn-spam {
@include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
@include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
}
&.btn-danger,
......@@ -199,7 +205,7 @@
}
.fa-caret-down,
.fa-caret-up {
.fa-chevron-down {
margin-left: 5px;
}
......@@ -351,7 +357,7 @@
.btn-inverted {
&-secondary {
@include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
@include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal);
}
}
......
......@@ -8,6 +8,12 @@
}
}
@mixin chevron-active {
.fa-chevron-down {
color: $dropdown-toggle-hover-icon-color;
}
}
.open {
.dropdown-menu,
.dropdown-menu-nav {
......@@ -19,53 +25,27 @@
}
}
.dropdown-toggle,
.dropdown-menu-toggle {
@include chevron-active;
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu-toggle {
position: relative;
width: 160px;
padding: 6px 20px 6px 10px;
.dropdown-toggle {
padding: 6px 8px 6px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $border-color;
border-radius: $border-radius-base;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.fa {
position: absolute;
top: 10px;
right: 8px;
color: $dropdown-toggle-icon-color;
&.fa-spinner {
font-size: 16px;
margin-top: -8px;
}
}
&.no-outline {
outline: 0;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
&.large {
width: 200px;
}
......@@ -86,6 +66,51 @@
max-width: 100%;
padding-right: 25px;
}
.fa {
color: $dropdown-toggle-icon-color;
}
.fa-chevron-down {
font-size: $dropdown-chevron-size;
position: relative;
top: -3px;
margin-left: 5px;
}
&:hover {
@include chevron-active;
border-color: $dropdown-toggle-hover-border-color;
}
&:focus:active {
@include chevron-active;
border-color: $dropdown-toggle-active-border-color;
}
}
.dropdown-menu-toggle {
@extend .dropdown-toggle;
padding-right: 20px;
position: relative;
width: 160px;
text-overflow: ellipsis;
overflow: hidden;
.fa {
position: absolute;
&.fa-spinner {
font-size: 16px;
margin-top: -8px;
}
}
.fa-chevron-down {
position: absolute;
top: 11px;
right: 8px;
}
}
.dropdown-menu,
......
......@@ -51,19 +51,26 @@
margin-bottom: -1px;
font-size: 15px;
line-height: 28px;
color: #959494;
color: $note-toolbar-color;
border-bottom: 2px solid transparent;
&:hover,
&:active,
&:focus {
text-decoration: none;
border-bottom: 2px solid $gray-darkest;
color: $black;
.badge {
color: $black;
}
}
}
&.active a {
border-bottom: 2px solid $link-underline-blue;
color: $black;
font-weight: 600;
}
.badge {
......@@ -85,14 +92,20 @@
li {
&.active a {
border-bottom: none;
color: $link-underline-blue;
}
a {
margin: 0;
padding: 11px 10px 9px;
}
&.active a {
&:hover,
&:active,
&:focus {
border-bottom: none;
color: $link-underline-blue;
}
}
}
}
......@@ -310,37 +323,9 @@
height: 51px;
li {
a {
padding-top: 10px;
}
a,
i {
color: $layout-link-gray;
}
&.active {
a,
i {
color: $black;
}
svg {
path,
polygon {
fill: $black;
}
}
}
&:hover {
a,
i {
color: $black;
}
}
}
}
}
......
......@@ -12,67 +12,71 @@ $sidebar-breakpoint: 1024px;
/*
* Color schema
*/
$darken-normal-factor: 7%;
$darken-dark-factor: 10%;
$darken-border-factor: 5%;
$white-light: #fff;
$white-normal: #ededed;
$white-dark: #ececec;
$white-normal: darken($white-light, $darken-normal-factor);
$white-dark: darken($white-light, $darken-dark-factor);
$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
$gray-dark: #ededed;
$gray-normal: darken($gray-light, $darken-normal-factor);
$gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
$green-normal: #2faa60;
$green-dark: #2ca05b;
$green-light: #3cbd70;
$green-normal: darken($green-light, $darken-normal-factor);
$green-dark: darken($green-light, $darken-dark-factor);
$blue-light: #2ea8e5;
$blue-normal: #2d9fd8;
$blue-dark: #2897ce;
$blue-normal: darken($blue-light, $darken-normal-factor);
$blue-dark: darken($blue-light, $darken-dark-factor);
$blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
$blue-medium: darken($blue-medium-light, $darken-normal-factor);
$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor);
$blue-light-transparent: rgba(44, 159, 216, 0.05);
$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
$orange-normal: darken($orange-light, $darken-normal-factor);
$orange-dark: darken($orange-light, $darken-dark-factor);
$red-light: #e52c5a;
$red-normal: #d22852;
$red-dark: darken($red-normal, 5%);
$red-normal: darken($red-light, $darken-normal-factor);
$red-dark: darken($red-light, $darken-dark-factor);
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$border-white-light: #f1f2f4;
$border-white-normal: #d6dae2;
$border-white-dark: #c6cacf;
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
$border-white-dark: darken($white-dark, $darken-border-factor);
$border-gray-light: #dcdcdc;
$border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf;
$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-dark: darken($gray-dark, $darken-border-factor);
$border-green-extra-light: #9adb84;
$border-green-light: #2faa60;
$border-green-normal: #2ca05b;
$border-green-dark: #279654;
$border-green-light: darken($green-light, $darken-border-factor);
$border-green-normal: darken($green-normal, $darken-border-factor);
$border-green-dark: darken($green-dark, $darken-border-factor);
$border-blue-light: #2d9fd8;
$border-blue-normal: #2897ce;
$border-blue-dark: #258dc1;
$border-blue-light: darken($blue-light, $darken-border-factor);
$border-blue-normal: darken($blue-normal, $darken-border-factor);
$border-blue-dark: darken($blue-dark, $darken-border-factor);
$border-orange-light: #fc6d26;
$border-orange-normal: #ce5237;
$border-orange-dark: #c14e35;
$border-orange-light: darken($orange-light, $darken-border-factor);
$border-orange-normal: darken($orange-normal, $darken-border-factor);
$border-orange-dark: darken($orange-dark, $darken-border-factor);
$border-red-light: #d22852;
$border-red-normal: #ca264f;
$border-red-dark: darken($border-red-normal, 5%);
$border-red-light: darken($red-light, $darken-border-factor);
$border-red-normal: darken($red-normal, $darken-border-factor);
$border-red-dark: darken($red-dark, $darken-border-factor);
$help-well-bg: $gray-light;
$help-well-border: #e5e5e5;
......@@ -216,7 +220,7 @@ $dropdown-bg: #fff;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
$dropdown-border-color: rgba(#000, .1);
$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
......@@ -225,13 +229,15 @@ $dropdown-input-color: #555;
$dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-chevron-size: 10px;
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
$dropdown-toggle-border-color: #eaeaea;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-color: #5c5c5c;
$dropdown-toggle-border-color: #e5e5e5;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%);
$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%);
$dropdown-toggle-icon-color: #c4c4c4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%);
/*
* Buttons
......@@ -255,7 +261,7 @@ $search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px;
$location-badge-color: #aaa;
$location-badge-bg: $gray-normal;
$location-badge-bg: $dark-background-color;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
$location-icon-active-color: #807e7e;
......
......@@ -132,7 +132,7 @@
display: none;
}
.btn-clipboard {
.btn-clipboard:hover {
color: $gl-gray;
}
}
......@@ -235,6 +235,10 @@
padding-bottom: 10px;
color: #999;
&:hover {
color: $gl-gray;
}
span {
display: block;
margin-top: 0;
......@@ -244,15 +248,17 @@
display: none;
}
.avatar:hover {
border-color: #999;
}
.btn-clipboard {
border: none;
color: #999;
&:hover {
background: transparent;
}
i {
color: #999;
color: $gl-gray;
}
}
}
......
......@@ -876,3 +876,11 @@ pre.light-well {
pointer-events: none;
}
}
.variables-table {
table-layout: fixed;
.variable-key {
width: 30%;
}
}
......@@ -112,6 +112,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:koding_enabled,
:koding_url,
:email_author_in_body,
:html_emails_enabled,
:repository_checks_enabled,
:metrics_packet_size,
:send_user_confirmation_email,
......
module LfsHelper
include Gitlab::Routing.url_helpers
# This concern assumes:
# - a `#project` accessor
# - a `#user` accessor
# - a `#authentication_result` accessor
# - a `#can?(object, action, subject)` method
# - a `#ci?` method
# - a `#download_request?` method
# - a `#upload_request?` method
# - a `#has_authentication_ability?(ability)` method
module LfsRequest
extend ActiveSupport::Concern
included do
before_action :require_lfs_enabled!
before_action :lfs_check_access!
end
private
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
......@@ -17,35 +33,15 @@ module LfsHelper
return if download_request? && lfs_download_access?
return if upload_request? && lfs_upload_access?
if project.public? || (user && user.can?(:read_project, project))
render_lfs_forbidden
if project.public? || can?(user, :read_project, project)
lfs_forbidden!
else
render_lfs_not_found
end
end
def lfs_download_access?
return false unless project.lfs_enabled?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
end
def objects
@objects ||= (params[:objects] || []).to_a
end
def user_can_download_code?
has_authentication_ability?(:download_code) && can?(user, :download_code, project)
end
def build_can_download_code?
has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end
def lfs_upload_access?
return false unless project.lfs_enabled?
has_authentication_ability?(:push_code) && can?(user, :push_code, project)
def lfs_forbidden!
render_lfs_forbidden
end
def render_lfs_forbidden
......@@ -70,6 +66,30 @@ module LfsHelper
)
end
def lfs_download_access?
return false unless project.lfs_enabled?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
end
def lfs_upload_access?
return false unless project.lfs_enabled?
has_authentication_ability?(:push_code) && can?(user, :push_code, project)
end
def lfs_deploy_token?
authentication_result.lfs_deploy_token?(project)
end
def user_can_download_code?
has_authentication_ability?(:download_code) && can?(user, :download_code, project)
end
def build_can_download_code?
has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end
def storage_project
@storage_project ||= begin
result = project
......@@ -82,4 +102,8 @@ module LfsHelper
result
end
end
def objects
@objects ||= (params[:objects] || []).to_a
end
end
module ToggleAwardEmoji
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, only: [:toggle_award_emoji]
end
def toggle_award_emoji
authenticate_user!
name = params.require(:name)
if awardable.user_can_award?(current_user, name)
......
module WorkhorseRequest
extend ActiveSupport::Concern
included do
before_action :verify_workhorse_api!
end
private
def verify_workhorse_api!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
end
......@@ -6,9 +6,9 @@ class HelpController < ApplicationController
def index
@help_index = File.read(Rails.root.join('doc', 'README.md'))
# Prefix Markdown links with `help/` unless they already have been
# See http://rubular.com/r/ie2MlpdUMq
@help_index.gsub!(/(\]\()(\/?help\/)?([^\)\(]+\))/, '\1/help/\3')
# Prefix Markdown links with `help/` unless they are external links
# See http://rubular.com/r/MioSrVLK3S
@help_index.gsub!(%r{(\]\()(?!.+://)([^\)\(]+\))}, '\1/help/\2')
end
def show
......
......@@ -4,7 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
@user.remove_avatar!
@user.save
@user.reset_events_cache
redirect_to profile_path
end
......
......@@ -20,7 +20,6 @@ class Projects::AvatarsController < Projects::ApplicationController
@project.remove_avatar!
@project.save
@project.reset_events_cache
redirect_to edit_project_path(@project)
end
......
......@@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
before_action :from_merge_request, only: [:edit, :update]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
......@@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController
def update
@path = params[:file_path] if params[:file_path].present?
after_edit_path =
if from_merge_request && @target_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
end
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
......@@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
def from_merge_request
# If blob edit was initiated from merge request page
@from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
if from_merge_request && @target_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
end
end
def editor_variables
......
......@@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController
execute(branch_name, ref)
if params[:issue_iid]
issue = @project.issues.find_by(iid: params[:issue_iid])
issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
......
......@@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics!
def show
@cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params))
@cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params))
stats_values, cycle_analytics_json = generate_cycle_analytics_data
......
......@@ -18,6 +18,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private
def download_request?
raise NotImplementedError
end
def upload_request?
raise NotImplementedError
end
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
......@@ -130,10 +138,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController
authentication_result.ci?(project)
end
def lfs_deploy_token?
authentication_result.lfs_deploy_token?(project)
end
def authentication_has_download_access?
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
end
......@@ -149,8 +153,4 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def authentication_project
authentication_result.project
end
def verify_workhorse_api!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
end
# This file should be identical in GitLab Community Edition and Enterprise Edition
class Projects::GitHttpController < Projects::GitHttpClientController
before_action :verify_workhorse_api!
include WorkhorseRequest
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
......@@ -67,14 +65,18 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def render_denied
if user && user.can?(:read_project, project)
render plain: 'Access denied', status: :forbidden
if user && can?(user, :read_project, project)
render plain: access_denied_message, status: :forbidden
else
# Do not leak information about project existence
render_not_found
end
end
def access_denied_message
'Access denied'
end
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
......
class Projects::LfsApiController < Projects::GitHttpClientController
include LfsHelper
include LfsRequest
before_action :require_lfs_enabled!
before_action :lfs_check_access!, except: [:deprecated]
skip_before_action :lfs_check_access!, only: [:deprecated]
def batch
unless objects.present?
......@@ -31,6 +30,14 @@ class Projects::LfsApiController < Projects::GitHttpClientController
private
def download_request?
params[:operation] == 'download'
end
def upload_request?
params[:operation] == 'upload'
end
def existing_oids
@existing_oids ||= begin
storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
......@@ -79,12 +86,4 @@ class Projects::LfsApiController < Projects::GitHttpClientController
}
}
end
def download_request?
params[:operation] == 'download'
end
def upload_request?
params[:operation] == 'upload'
end
end
class Projects::LfsStorageController < Projects::GitHttpClientController
include LfsHelper
include LfsRequest
include WorkhorseRequest
before_action :require_lfs_enabled!
before_action :lfs_check_access!
before_action :verify_workhorse_api!, only: [:upload_authorize]
skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
def download
lfs_object = LfsObject.find_by_oid(oid)
......
......@@ -329,17 +329,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
unless @merge_request.pipeline
unless @merge_request.head_pipeline
@status = :failed
return
end
if @merge_request.pipeline.active?
if @merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
elsif @merge_request.pipeline.success?
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)
......@@ -403,7 +404,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
pipeline = @merge_request.pipeline
pipeline = @merge_request.head_pipeline
if pipeline
status = pipeline.status
coverage = pipeline.try(:coverage)
......@@ -539,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_widget_vars
@pipeline = @merge_request.pipeline
@pipeline = @merge_request.head_pipeline
end
def define_commit_vars
......@@ -568,8 +570,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars
@pipelines = @merge_request.all_pipelines
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline.present?
@pipeline = @merge_request.head_pipeline
@statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
end
def define_new_vars
......@@ -636,7 +638,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_when_build_succeeds_active?
params[:merge_when_build_succeeds].present? &&
@merge_request.pipeline && @merge_request.pipeline.active?
@merge_request.head_pipeline && @merge_request.head_pipeline.active?
end
def build_merge_request
......
......@@ -16,13 +16,7 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin
case params[:issuable_type]
when "issue"
issue = @project.issues.find(params[:issuable_id])
if can?(current_user, :read_issue, issue)
issue
else
render_404
end
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
......
......@@ -16,14 +16,12 @@
# label_name: string
# sort: string
#
require_relative 'projects_finder'
class IssuableFinder
NONE = '0'
attr_accessor :current_user, :params
def initialize(current_user, params)
def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
......@@ -43,6 +41,14 @@ class IssuableFinder
sort(items)
end
def find(*params)
execute.find(*params)
end
def find_by(*params)
execute.find_by(*params)
end
def group
return @group if defined?(@group)
......
......@@ -12,7 +12,7 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
......
......@@ -43,7 +43,7 @@ module DropdownsHelper
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('caret-down')
output << icon('chevron-down')
output.html_safe
end
end
......
......@@ -8,11 +8,7 @@ module GroupsHelper
group = Group.find_by(path: group)
end
if group && group.avatar.present?
group.avatar.url
else
image_path('no_group_avatar.png')
end
group.try(:avatar_url) || image_path('no_group_avatar.png')
end
def group_title(group, name = nil, url = nil)
......
......@@ -143,6 +143,20 @@ module IssuablesHelper
end
end
def issuable_filter_params
[
:search,
:author_id,
:assignee_id,
:milestone_title,
:label_name
]
end
def issuable_filter_present?
issuable_filter_params.any? { |k| params.key?(k) }
end
private
def assigned_issuables_count(assignee, issuable_type, state)
......@@ -165,10 +179,6 @@ module IssuablesHelper
end
end
def issuable_filters_present
params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
end
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
......
......@@ -4,6 +4,7 @@ module Emails
setup_note_mail(note_id, recipient_id)
@commit = @note.noteable
@discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit,
......@@ -24,6 +25,7 @@ module Emails
setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable
@discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
......
......@@ -317,7 +317,7 @@ module Ci
def merge_requests
@merge_requests ||= project.merge_requests
.where(source_branch: self.ref)
.select { |merge_request| merge_request.pipeline.try(:id) == self.id }
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end
private
......
......@@ -2,6 +2,9 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
belongs_to :protected_branch
delegate :project, to: :protected_branch
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
......@@ -9,4 +12,10 @@ module ProtectedBranchAccess
def humanize
self.class.human_access_levels[self.access_level]
end
def check_access(user)
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level
end
end
class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, from:)
def initialize(project, current_user, from:)
@project = project
@current_user = current_user
@from = from
@fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
end
def summary
@summary ||= Summary.new(@project, from: @from)
@summary ||= Summary.new(@project, @current_user, from: @from)
end
def permissions(user:)
......
class CycleAnalytics
class Summary
def initialize(project, from:)
def initialize(project, current_user, from:)
@project = project
@current_user = current_user
@from = from
end
def new_issues
@project.issues.created_after(@from).count
IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
end
def commits
......
......@@ -25,7 +25,12 @@ class Discussion
to: :last_resolved_note,
allow_nil: true
delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
delegate :blob,
:highlighted_diff_lines,
:diff_lines,
to: :diff_file,
allow_nil: true
def self.for_notes(notes)
notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
......@@ -159,10 +164,11 @@ class Discussion
end
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
prev_lines = []
highlighted_diff_lines.each do |line|
lines.each do |line|
if line.meta?
prev_lines.clear
else
......
......@@ -43,12 +43,6 @@ class Event < ActiveRecord::Base
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self
def reset_event_cache_for(target)
Event.where(target_id: target.id, target_type: target.class.to_s).
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type in (?) AND action in (?))",
......@@ -353,6 +347,10 @@ class Event < ActiveRecord::Base
update_all(last_activity_at: created_at)
end
def authored_by?(user)
user ? author_id == user.id : false
end
private
def recent_update?
......
......@@ -182,18 +182,6 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
# Reset issue events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when an issue is updated
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.reset_event_cache_for(self)
end
# To allow polymorphism with MergeRequest.
def source_project
project
......
......@@ -605,18 +605,6 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_names.include?(self.target_branch)
end
# Reset merge request events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when a merge request is updated
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.reset_event_cache_for(self)
end
def merge_commit_message
message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
message << "#{title}\n\n"
......@@ -690,7 +678,7 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_build_succeeds?
!pipeline || pipeline.success? || pipeline.skipped?
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
def environments
......@@ -786,14 +774,14 @@ class MergeRequest < ActiveRecord::Base
commits.map(&:sha)
end
def pipeline
def head_pipeline
return unless diff_head_sha && source_project
@pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
@head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines
return unless source_project
return Ci::Pipeline.none unless source_project
@all_pipelines ||= source_project.pipelines
.where(sha: all_commits_sha, ref: source_branch)
......
......@@ -201,19 +201,6 @@ class Note < ActiveRecord::Base
super(noteable_type.to_s.classify.constantize.base_class.to_s)
end
# Reset notes events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when a note is updated
# * when a note is removed
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.reset_event_cache_for(self)
end
def editable?
!system?
end
......
......@@ -687,9 +687,9 @@ class Project < ActiveRecord::Base
self.id
end
def get_issue(issue_id)
def get_issue(issue_id, current_user)
if default_issues_tracker?
issues.find_by(iid: issue_id)
IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
else
ExternalIssue.new(issue_id, self)
end
......@@ -976,7 +976,6 @@ class Project < ActiveRecord::Base
begin
gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
@old_path_with_namespace = old_path_with_namespace
......@@ -1043,22 +1042,6 @@ class Project < ActiveRecord::Base
attrs
end
# Reset events cache related to this project
#
# Since we do cache @event we need to reset cache in special cases:
# * when project was moved
# * when project was renamed
# * when the project avatar changes
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(project_id: self.id).
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
def project_member(user)
project_members.find_by(user_id: user)
end
......
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
belongs_to :protected_branch
delegate :project, to: :protected_branch
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER] }
......@@ -13,10 +10,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
Gitlab::Access::DEVELOPER => "Developers + Masters"
}.with_indifferent_access
end
def check_access(user)
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level
end
end
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
belongs_to :protected_branch
delegate :project, to: :protected_branch
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS] }
......@@ -18,8 +15,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level
super
end
end
......@@ -196,18 +196,12 @@ class Repository
options = { message: message, tagger: user_to_committer(user) } if message
rugged.tags.create(tag_name, target, options)
tag = find_tag(tag_name)
GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do
# we already created a tag, because we need tag SHA to pass correct
# values to hooks
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service|
raw_tag = rugged.tags.create(tag_name, target, options)
service.newrev = raw_tag.target_id
end
tag
rescue GitHooksService::PreReceiveError
rugged.tags.delete(tag_name)
raise
find_tag(tag_name)
end
def rm_branch(user, branch_name)
......
......@@ -445,13 +445,12 @@ class User < ActiveRecord::Base
end
def refresh_authorized_projects
loop do
begin
Gitlab::Database.serialized_transaction do
transaction do
project_authorizations.delete_all
# project_authorizations_union can return multiple records for the same project/user with
# different access_level so we take row with the maximum access_level
# project_authorizations_union can return multiple records for the same
# project/user with different access_level so we take row with the maximum
# access_level
project_authorizations.connection.execute <<-SQL
INSERT INTO project_authorizations (user_id, project_id, access_level)
SELECT user_id, project_id, MAX(access_level) AS access_level
......@@ -459,13 +458,8 @@ class User < ActiveRecord::Base
GROUP BY user_id, project_id
SQL
update_column(:authorized_projects_populated, true) unless authorized_projects_populated
end
break
# In the event of a concurrent modification Rails raises StatementInvalid.
# In this case we want to keep retrying until the transaction succeeds
rescue ActiveRecord::StatementInvalid
unless authorized_projects_populated
update_column(:authorized_projects_populated, true)
end
end
end
......@@ -708,20 +702,6 @@ class User < ActiveRecord::Base
project.project_member(self)
end
# Reset project events cache related to this user
#
# Since we do cache @event we need to reset cache in special cases:
# * when the user changes their avatar
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(author_id: id).
order('id DESC').limit(1000).
update_all(updated_at: Time.now)
end
def full_website_url
return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
......
......@@ -13,7 +13,7 @@ class AnalyticsBuildEntity < Grape::Entity
end
expose :duration, as: :total_time do |build|
distance_of_time_as_hash(build.duration.to_f)
build.duration ? distance_of_time_as_hash(build.duration.to_f) : {}
end
expose :branch do
......
......@@ -16,6 +16,9 @@ class BuildEntity < Grape::Entity
path_to(:play_namespace_project_build, build)
end
expose :created_at
expose :updated_at
private
def path_to(route, build)
......
......@@ -2,6 +2,8 @@ module EntityDateHelper
include ActionView::Helpers::DateHelper
def interval_in_words(diff)
return 'Not started' unless diff
"#{distance_of_time_in_words(Time.now, diff)} ago"
end
......
......@@ -45,9 +45,15 @@ module Ci
return error('No builds for this pipeline.')
end
Ci::Pipeline.transaction do
pipeline.save
pipeline.process!
pipeline
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
pipeline.tap(&:process!)
end
private
......
......@@ -5,10 +5,7 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
# This method will ensure that our pipeline does have all builds for all stages created
if created_builds.empty?
create_builds!
end
ensure_created_builds! # TODO, remove me in 9.0
new_builds =
stage_indexes_of_created_builds.map do |index|
......@@ -22,10 +19,6 @@ module Ci
private
def create_builds!
Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
end
def process_stage(index)
current_status = status_for_prior_stages(index)
......@@ -76,5 +69,18 @@ module Ci
def created_builds
pipeline.builds.created
end
# This method is DEPRECATED and should be removed in 9.0.
#
# We need it to maintain backwards compatibility with previous versions
# when builds were not created within one transaction with the pipeline.
#
def ensure_created_builds!
return if created_builds.any?
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
end
end
class GitHooksService
PreReceiveError = Class.new(StandardError)
attr_accessor :oldrev, :newrev, :ref
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
@user = Gitlab::GlId.gl_id(user)
......@@ -16,7 +18,7 @@ class GitHooksService
end
end
yield
yield self
run_hook('post-receive')
end
......@@ -25,6 +27,6 @@ class GitHooksService
def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path)
hook.trigger(@user, @oldrev, @newrev, @ref)
hook.trigger(@user, oldrev, newrev, ref)
end
end
......@@ -85,14 +85,15 @@ class IssuableBaseService < BaseService
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
params[:label_ids] = labels.split(',').map do |label_name|
params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
label.id
end
label.try(:id)
end.compact
end
def process_label_ids(attributes, existing_label_ids: nil)
......@@ -140,6 +141,7 @@ class IssuableBaseService < BaseService
params.delete(:state_event)
params[:author] ||= current_user
label_ids = process_label_ids(params)
issuable.assign_attributes(params)
......@@ -184,8 +186,6 @@ class IssuableBaseService < BaseService
params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache
# 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)
......
......@@ -22,9 +22,14 @@ module Labels
).execute(skip_authorization: skip_authorization)
end
# Only creates the label if current_user can do so, if the label does not exist
# and the user can not create the label, nil is returned
def find_or_create_label
new_label = available_labels.find_by(title: title)
new_label ||= project.labels.create(params)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
new_label = project.labels.create(params)
end
new_label
end
......
......@@ -55,7 +55,7 @@ module MergeRequests
def pipeline_merge_requests(pipeline)
merge_requests_for(pipeline.ref).each do |merge_request|
next unless pipeline == merge_request.pipeline
next unless pipeline == merge_request.head_pipeline
yield merge_request
end
......@@ -63,7 +63,7 @@ module MergeRequests
def commit_status_merge_requests(commit_status)
merge_requests_for(commit_status.ref).each do |merge_request|
pipeline = merge_request.pipeline
pipeline = merge_request.head_pipeline
next unless pipeline
next unless pipeline.sha == commit_status.sha
......
......@@ -81,7 +81,7 @@ module MergeRequests
commit = commits.first
merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip)
elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?)
elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
case issue
when Issue
merge_request.title = "Resolve \"#{issue.title}\""
......
......@@ -2,7 +2,6 @@ module Notes
class DeleteService < BaseService
def execute(note)
note.destroy
note.reset_events_cache
end
end
end
......@@ -5,7 +5,6 @@ module Notes
note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
note.reset_events_cache
if note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user)
......
......@@ -61,9 +61,6 @@ module Projects
# Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute
# clear project cached events
project.reset_events_cache
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
......
......@@ -3,16 +3,10 @@ class AvatarUploader < CarrierWave::Uploader::Base
storage :file
after :store, :reset_events_cache
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def reset_events_cache(file)
model.reset_events_cache if model.is_a?(User)
end
def exists?
model.avatar.file && model.avatar.file.exists?
end
......
......@@ -443,7 +443,16 @@
Some email servers do not support overriding the email sender name.
Enable this option to include the name of the author of the issue,
merge request or comment in the email body instead.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :html_emails_enabled do
= f.check_box :html_emails_enabled
Enable HTML emails
.help-block
By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only
want to send emails in plain text format.
%fieldset
%legend Automatic Git repository housekeeping
.form-group
......
......@@ -4,13 +4,13 @@
%ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
Your projects
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
Starred Projects
Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore Projects
Explore projects
.nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
......
......@@ -4,6 +4,18 @@
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
- if current_user.can_create_group?
.blank-state
.blank-state-icon
= custom_icon("group", size: 50)
%h3.blank-state-title
You can create a group for several dependent projects.
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "btn btn-new" do
New group
.blank-state
.blank-state-icon
= custom_icon("project", size: 50)
......@@ -21,17 +33,6 @@
= link_to new_project_path, class: "btn btn-new" do
New project
- if current_user.can_create_group?
.blank-state
.blank-state-icon
= custom_icon("group", size: 50)
%h3.blank-state-title
You can create a group for several dependent projects.
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "btn btn-new" do
New group
-if publicish_project_count > 0
.blank-state
.blank-state-icon
......
......@@ -50,13 +50,13 @@
data: { data: todo_actions_options }})
.pull-right
.dropdown.inline.prepend-left-10
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
......
......@@ -3,7 +3,6 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
= cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40)
- if event.created_project?
......
......@@ -18,7 +18,7 @@
- few_commits.each do |commit|
= render "events/commit", commit: commit, project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project)
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- if event.commits_count > 2
......@@ -35,12 +35,12 @@
Compare #{from_label}...#{truncate_sha(event.commit_to)}
- if create_mr
%span{"data-user-is" => event.author_id, "data-display" => "inline"}
%span
or
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
create a merge request
- elsif create_mr
%li.commits-stat{"data-user-is" => event.author_id}
%li.commits-stat
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
Create Merge Request
- elsif event.rm_ref?
......
......@@ -17,13 +17,13 @@
.pull-right
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
......
- if current_user
.dropdown
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
Any
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
......@@ -20,14 +20,14 @@
- if @tags.present?
.dropdown
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('tags')
%span.light Tags:
- if params[:tag].present?
= params[:tag]
- else
Any
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
......
......@@ -20,11 +20,18 @@
= link_to group.name, group_url(group)
as #{@member.human_access}.
- if @member.source.users.include?(current_user)
- is_member = @member.source.users.include?(current_user)
- if is_member
%p
However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
Sign in using a different account to accept the invitation.
- else
- if @member.invite_email != current_user.email
%p
Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}.
- unless is_member
.actions
= link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
= link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
......@@ -56,5 +56,3 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render 'layouts/bootlint' if Rails.env.development?
= render 'layouts/user_styles'
:css
[data-user-is] {
display: none !important;
}
[data-user-is="#{current_user.try(:id)}"] {
display: block !important;
}
[data-user-is="#{current_user.try(:id)}"][data-display="inline"] {
display: inline !important;
}
[data-user-is-not] {
display: block !important;
}
[data-user-is-not][data-display="inline"] {
display: inline !important;
}
[data-user-is-not="#{current_user.try(:id)}"] {
display: none !important;
}
......@@ -70,7 +70,7 @@
%span
Issues
- if @project.default_issues_tracker?
%span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
......
<% if current_application_settings.email_author_in_body %>
<%= @note.author_name %> wrote:
<% end -%>
<%= @note.note %>
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
New comment
- if @discussion && @discussion.diff_file
on
= link_to @note.diff_file.file_path, @target_url, class: 'details'
\:
%table
= render partial: "projects/diffs/line",
collection: @discussion.truncated_diff_lines,
as: :line,
locals: { diff_file: @note.diff_file,
plain: true,
email: true }
= render 'note_message'
<% if @discussion && @discussion.diff_file -%>
on <%= @note.diff_file.file_path -%>
<% end -%>:
<%= url %>
<%= render 'simple_diff' if @discussion -%>
<%= render 'note_message' %>
<% @discussion.truncated_diff_lines(highlight: false).each do |line| %>
> <%= line.text %>
<% end %>
= render 'note_message'
%p.details
= render 'note_mr_or_commit_email'
New comment for Commit <%= @commit.short_id %>
<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %>
Author: <%= @note.author_name %>
<%= @note.note %>
New comment for Commit <%= @commit.short_id -%>
<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %>
- if @note.diff_note? && @note.diff_file
%p.details
New comment on diff for
= link_to @note.diff_file.file_path, @target_url
\:
= render 'note_message'
%p.details
= render 'note_mr_or_commit_email'
New comment for Merge Request <%= @merge_request.to_reference %>
<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %>
<%= @note.author_name %>
<%= @note.note %>
New comment for Merge Request <%= @merge_request.to_reference -%>
<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%>
= content_for :head do
= stylesheet_link_tag 'mailers/repository_push_email'
= stylesheet_link_tag 'mailers/highlighted_diff_email'
%h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
......
......@@ -27,5 +27,5 @@
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
......@@ -12,10 +12,10 @@
= search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_branches_path(sort: sort_value_name) do
......
......@@ -116,7 +116,7 @@
.title Stage
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.stage-selection More
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.stages.each do |stage|
%li
......
......@@ -9,7 +9,7 @@
= icon('comment')
\
- if editable_diff?(diff_file)
- link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
......
......@@ -25,7 +25,7 @@
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
%pre= diff_line_content(line.text)
%pre= line.text
- else
= diff_line_content(line.text)
......
......@@ -9,13 +9,13 @@
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
%button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'}
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
= icon('caret-down')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
......
......@@ -2,12 +2,12 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- has_any_ci = @merge_requests.any?(&:pipeline)
- has_any_ci = @merge_requests.any?(&:head_pipeline)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- if merge_request.pipeline
= render_pipeline_status(merge_request.pipeline)
- if merge_request.head_pipeline
= render_pipeline_status(merge_request.head_pipeline)
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
......
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.
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