提交 35659da1 编写于 作者: C Clement Ho

Merge branch 'master' into 'fix-btn-alignment'

# Conflicts:
#   app/views/projects/merge_requests/_nav_btns.html.haml
......@@ -226,6 +226,7 @@ update-tests-metadata:
flaky-examples-check:
<<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine
services: []
before_script: []
......
......@@ -324,6 +324,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
......
......@@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
actionmailer (4.2.8)
actionpack (= 4.2.8)
......@@ -41,6 +42,9 @@ GEM
tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0)
activerecord (>= 4.0)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
......@@ -124,6 +128,9 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.7.7)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5)
......@@ -470,6 +477,8 @@ GEM
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
......@@ -610,6 +619,11 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
proc_to_ast (0.1.0)
coderay
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta11)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
......@@ -718,6 +732,10 @@ GEM
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
......@@ -726,6 +744,12 @@ GEM
rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (3.6.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
......@@ -892,6 +916,14 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.6)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5)
procto (~> 0.0.2)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
......@@ -1094,6 +1126,7 @@ DEPENDENCIES
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
......
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({
url: form.data('signatures-path'),
data: form.serialize(),
}).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
......
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
template: `
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i class="fa fa-times"></i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
`,
};
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
created() {
this.illustrationSvg = illustrationSvg;
},
};
</script>
<template>
<div
v-if="!calloutDismissed"
class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i
aria-hidden="true"
class="fa fa-times">
</i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
......
......@@ -48,6 +48,27 @@
return `${this.job.name} - ${this.job.status.label}`;
},
},
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
},
mounted() {
this.stopDropdownClickPropagation();
},
};
</script>
<template>
......
......@@ -29,12 +29,10 @@ export default {
editMode() {
if (this.editMode) {
$('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show();
$('.project-refs-target-form').show();
$('.js-tree-ref-target-holder').show();
} else {
$('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide();
$('.project-refs-target-form').hide();
$('.js-tree-ref-target-holder').hide();
}
},
},
......
......@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default {
data: () => Store,
mounted() {
$(this.$el).find('.file-content').syntaxHighlight();
this.highlightFile();
},
computed: {
html() {
......@@ -12,10 +12,16 @@ export default {
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: {
html() {
this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight();
this.highlightFile();
});
},
},
......@@ -24,9 +30,23 @@ export default {
<template>
<div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
<div v-if="activeFile.render_error" class="vertical-center render-error">
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
<div
v-if="!activeFile.render_error"
v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
</div>
</template>
......@@ -33,32 +33,30 @@ const RepoSidebar = {
});
},
linkClicked(clickedFile) {
let url = '';
fileClicked(clickedFile) {
let file = clickedFile;
if (typeof file === 'object') {
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
url = file.url;
Service.url = url;
// I need to refactor this to do the `then` here.
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
});
}
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
})
.catch(Helper.loadingError);
}
},
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
},
};
......@@ -82,7 +80,7 @@ export default RepoSidebar;
<repo-previous-directory
v-if="isRoot"
:prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/>
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file
v-for="n in 5"
:key="n"
......@@ -94,7 +92,7 @@ export default RepoSidebar;
:key="file.id"
:file="file"
:is-mini="isMini"
@linkclicked="linkClicked(file)"
@linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"/>
......
......@@ -10,6 +10,12 @@ const RepoTab = {
},
computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() {
const tabChangedObj = {
'fa-times': !this.tab.changed,
......@@ -34,12 +40,24 @@ export default RepoTab;
<template>
<li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
<i class="fa" :class="changedClass"></i>
<a
href="#0"
class="close"
@click.prevent="xClicked(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
<a
href="#"
class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li>
</template>
<script>
import Vue from 'vue';
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
......@@ -14,29 +13,19 @@ const RepoTabs = {
data: () => Store,
methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) {
Store.removeFromOpenedFiles(file);
},
},
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
};
export default RepoTabs;
</script>
<template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
<ul
v-if="isMini"
id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" />
</ul>
......
......@@ -10,7 +10,10 @@ function repoEditorLoader() {
Store.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, reject);
}, () => {
Store.monacoLoading = false;
reject();
});
});
}
......
......@@ -33,12 +33,16 @@ const RepoHelper = {
? window.performance
: Date,
getFileExtension(fileName) {
return fileName.split('.').pop();
},
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
},
getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop();
const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext';
......@@ -135,21 +139,19 @@ const RepoHelper = {
return isRoot;
},
getContent(treeOrFile, cb) {
getContent(treeOrFile) {
let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
// RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
Store.binary = data.binary;
if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
......@@ -188,9 +190,8 @@ const RepoHelper = {
setFile(data, file) {
const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url;
if (newFile.render_error === 'too_large') {
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
newFile.newContent = '';
......@@ -199,10 +200,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile);
},
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
......@@ -226,7 +223,7 @@ const RepoHelper = {
type,
name,
url,
icon: RepoHelper.toFA(icon),
icon: `fa-${icon}`,
level: 0,
loading: false,
};
......@@ -244,7 +241,7 @@ const RepoHelper = {
setTimeout(() => {
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = 12000;
tabs.scrollLeft = tabs.scrollWidth;
}, 200);
},
......
......@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.project-refs-target-form').hide();
$('.fa-long-arrow-right').hide();
$('.js-tree-ref-target-holder').hide();
}
function addEventsForNonVueEls() {
......
......@@ -2,6 +2,7 @@
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = {
url: '',
......@@ -22,6 +23,7 @@ const RepoService = {
getRaw(url) {
return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res],
});
},
......@@ -36,7 +38,7 @@ const RepoService = {
},
urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop();
const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension);
},
......
......@@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
ideEl: {},
monaco: {},
monacoLoading: false,
monacoInstance: {},
service: '',
editor: '',
sidebar: '',
editMode: false,
isTree: false,
isRoot: false,
......@@ -17,19 +14,10 @@ const RepoStore = {
projectId: '',
projectName: '',
projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview',
openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false,
binaryLoaded: false,
dialog: {
open: false,
title: '',
......@@ -45,9 +33,6 @@ const RepoStore = {
currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: {
png: false,
md: false,
......@@ -58,7 +43,6 @@ const RepoStore = {
tree: false,
blob: false,
},
readOnly: true,
resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => {
......@@ -96,7 +80,6 @@ const RepoStore = {
if (file.binary) {
RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain;
} else {
......@@ -238,4 +221,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview';
},
};
export default RepoStore;
......@@ -71,7 +71,7 @@ export default {
/>
<div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i>
None
This issue is not confidential
</div>
<div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
......
......@@ -286,6 +286,10 @@
.gpg-status-box {
&:empty {
display: none;
}
&.valid {
@include green-status-color;
}
......
......@@ -453,7 +453,10 @@ ul.notes {
}
.note-actions {
align-self: flex-start;
flex-shrink: 0;
display: inline-flex;
align-items: center;
// For PhantomJS that does not support flex
float: right;
margin-left: 10px;
......@@ -463,18 +466,12 @@ ul.notes {
float: none;
margin-left: 0;
}
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
}
.more-actions {
display: inline-block;
float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
.tooltip {
white-space: nowrap;
......@@ -482,16 +479,10 @@ ul.notes {
}
.more-actions-toggle {
padding: 0;
&:hover .icon,
&:focus .icon {
color: $blue-600;
}
.icon {
padding: 0 6px;
}
}
.more-actions-dropdown {
......@@ -519,28 +510,42 @@ ul.notes {
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
}
.note-action-button {
margin-left: 0;
}
.note-actions-item {
margin-left: 15px;
display: flex;
align-items: center;
&.more-actions {
// compensate for narrow icon
margin-left: 10px;
}
}
.note-action-button {
display: inline;
line-height: 20px;
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-darkest;
.fa {
color: $gray-darkest;
position: relative;
font-size: 17px;
font-size: 16px;
}
svg {
height: 16px;
width: 16px;
fill: $gray-darkest;
top: 0;
vertical-align: text-top;
path {
fill: currentColor;
}
}
.award-control-icon-positive,
......@@ -613,10 +618,7 @@ ul.notes {
.note-role {
position: relative;
top: -2px;
display: inline-block;
padding-left: 7px;
padding-right: 7px;
padding: 0 7px;
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
......
......@@ -29,6 +29,10 @@
margin-right: 15px;
}
.tree-ref-target-holder {
display: inline-block;
}
.repo-breadcrumb {
li:last-of-type {
position: relative;
......
......@@ -6,6 +6,13 @@ module CycleAnalyticsParams
end
def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago
case params[:start_date]
when '7'
7.days.ago
when '30'
30.days.ago
else
90.days.ago
end
end
end
......@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create_merge_request
result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
......@@ -59,7 +59,7 @@ module GroupsHelper
end
def remove_group_message(group)
_("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") %
_("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end
......
......@@ -80,7 +80,7 @@ module ProjectsHelper
end
def remove_project_message(project)
_("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") %
_("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace }
end
......
......@@ -11,11 +11,11 @@ module Emails
@member_source_type = member_source_type
@member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
admins = member_source.members.owners_and_masters.pluck(:notification_email)
# A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group
admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
admins = member_source.group.members.owners_and_masters.pluck(:notification_email)
end
mail(to: admins,
......
......@@ -212,21 +212,39 @@ class Group < Namespace
end
def user_ids_for_project_authorizations
users_with_parents.pluck(:id)
members_with_parents.pluck(:user_id)
end
def members_with_parents
GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil)
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
if parent_id
self_and_ancestors.reorder(nil).select(:id)
else
id
end
GroupMember
.active_without_invites
.where(source_id: source_ids)
end
def members_with_descendants
GroupMember
.active_without_invites
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end
def users_with_parents
User.where(id: members_with_parents.select(:user_id))
User
.where(id: members_with_parents.select(:user_id))
.reorder(nil)
end
def users_with_descendants
members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id))
User.where(id: members_with_descendants.select(:user_id))
User
.where(id: members_with_descendants.select(:user_id))
.reorder(nil)
end
def max_member_access_for_user(user)
......
......@@ -41,9 +41,20 @@ class Member < ActiveRecord::Base
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_active = User.arel_table[:state].eq(:active)
includes(:user).references(:users)
.where(is_external_invite.or(user_is_active))
user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
left_join_users
.where(user_ok)
.where(requested_at: nil)
.reorder(nil)
end
# Like active, but without invites. For when a User is required.
scope :active_without_invites, -> do
left_join_users
.where(users: { state: 'active' })
.where(requested_at: nil)
.reorder(nil)
end
scope :invite, -> { where.not(invite_token: nil) }
......
......@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
end
def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed?
if (source_branch_changed? || target_branch_changed?) &&
(source_branch_head && target_branch_head)
reload_diff
end
end
......@@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base
end
def fetch_ref
target_project.repository.fetch_ref(
source_project.repository.path_to_repo,
"refs/heads/#{source_branch}",
ref_path
)
write_ref
update_column(:ref_fetched, true)
end
......@@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base
true
end
private
def write_ref
target_project.repository.with_repo_branch_commit(
source_project.repository, source_branch) do |commit|
if commit
target_project.repository.write_ref(ref_path, commit.sha)
else
raise Rugged::ReferenceError, 'source repository is empty'
end
end
end
end
......@@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors
end
def self_and_ancestors
return self.class.where(id: id) unless parent_id
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_ancestors
end
# Returns all the descendants of the current namespace.
def descendants
Gitlab::GroupHierarchy
......@@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base
.base_and_descendants
end
def self_and_descendants
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_descendants
end
def user_ids_for_project_authorizations
[owner_id]
end
......
......@@ -196,7 +196,6 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
......@@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base
def change_head(branch)
if repository.branch_exists?(branch)
repository.before_change_head
repository.rugged.references.create('HEAD',
"refs/heads/#{branch}",
force: true)
repository.write_ref('HEAD', "refs/heads/#{branch}")
repository.copy_gitattributes(branch)
repository.after_change_head
reload_default_branch
......@@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base
# @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path
def forks_count
Projects::ForksCountService.new(self).count
end
private
def cross_namespace_reference?(from)
......
......@@ -224,7 +224,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
begin
rugged.references.create(keep_around_ref_name(sha), sha, force: true)
write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex
......@@ -237,6 +237,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha))
end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
......@@ -985,12 +989,10 @@ class Repository
if start_repository == self
start_branch_name
else
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
fetch_ref(
tmp_ref = fetch_ref(
start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
tmp_ref
"refs/tmp/#{SecureRandom.hex}/head"
)
start_repository.commit(start_branch_name).sha
......@@ -1021,7 +1023,12 @@ class Repository
def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args)
message, status = run_git(args)
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
target_ref
end
def create_ref(ref, ref_path)
......
......@@ -726,9 +726,9 @@ class User < ActiveRecord::Base
end
def sanitize_attrs
%w[username skype linkedin twitter].each do |attr|
value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend
public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend
%i[skype linkedin twitter].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
end
......
......@@ -128,6 +128,8 @@ module Projects
project.repository.before_delete
Repository.new(wiki_path, project, disk_path: repo_path).before_delete
Projects::ForksCountService.new(project).delete_cache
end
end
end
......@@ -21,11 +21,17 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
new_project
end
private
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
def allowed_visibility_level
project_level = @project.visibility_level
......
module Projects
# Service class for getting and caching the number of forks of a project.
class ForksCountService
def initialize(project)
@project = project
end
def count
Rails.cache.fetch(cache_key) { uncached_count }
end
def refresh_cache
Rails.cache.write(cache_key, uncached_count)
end
def delete_cache
Rails.cache.delete(cache_key)
end
private
def uncached_count
@project.forks.count
end
def cache_key
['projects', @project.id, 'forks_count']
end
end
end
......@@ -13,7 +13,13 @@ module Projects
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy
end
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
end
end
- if commit.has_signature?
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
%i.fa.fa-spinner.fa-spin
......@@ -39,6 +39,9 @@
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
%li
%a{ "href" => "#", "data-value" => "30" }
{{ n__('Last %d day', 'Last %d days', 30) }}
......
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }),
......
- if @can_bulk_update
= button_tag "Edit Merge Requests", class: "btn append-right-10 js-bulk-update-toggle"
= button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle"
- if merge_project
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
New merge request
......@@ -17,24 +17,32 @@
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
.note-actions-item
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
.note-actions-item
= button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
- if note_editable
.note-actions-item
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
- is_current_user = current_user == note.author
- if note_editable || !is_current_user
.dropdown.more-actions
.dropdown.more-actions.note-actions-item
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do
= icon('ellipsis-v', class: 'icon')
%span.icon
= custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
- if note_editable
%li
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
%li.divider
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
......@@ -2,8 +2,9 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo?
= icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path
.tree-ref-target-holder.js-tree-ref-target-holder
= icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path
- unless show_new_repo?
= render 'projects/tree/old_tree_header'
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1600"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></svg>
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
.note-actions-item
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
.note-actions-item
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
---
title: Improves performance of vue code by using vue files and moving svg out of data
function in pipeline schedule callout
merge_request:
author:
type: other
---
title: move edit comment button outside of dropdown
merge_request:
author:
---
title: Fix timeouts when creating projects in groups with many members
merge_request: 13508
author:
type: fixed
---
title: Prevents jobs dropdown from closing in pipeline graph
merge_request:
author:
type: fixed
---
title: Fix edit merge request and issues button inconsistent letter casing
merge_request:
author:
type: fixed
---
title: Cache the number of forks of a project
merge_request: 13535
author:
type: other
---
title: Add a `Last 7 days` option for Cycle Analytics view
merge_request: 13443
author: Mehdi Lahmam (@mehlah)
type: added
- group: Response metrics (NGINX Ingress)
priority: 10
metrics:
- title: "Throughput"
y_label: "Requests / Sec"
required_metrics:
- nginx_upstream_requests_total
weight: 1
queries:
- query_range: 'sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
label: Total
unit: req / sec
- title: "Latency"
y_label: "Latency (ms)"
required_metrics:
- nginx_upstream_response_msecs_avg
weight: 1
queries:
- query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})'
label: Average
unit: ms
- title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec"
required_metrics:
- nginx_upstream_responses_total
weight: 1
queries:
- query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- group: Response metrics (HA Proxy)
priority: 10
metrics:
......@@ -68,18 +98,18 @@
- nginx_upstream_response_msecs_avg
weight: 1
queries:
- query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000'
- query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})'
label: Upstream
unit: ms
- title: "HTTP Error Rate"
y_label: "Error Rate (%)"
y_label: "HTTP 500 Errors / Sec"
required_metrics:
- nginx_responses_total
weight: 1
queries:
- query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) / sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m]))'
- query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
unit: "%"
unit: "errors / sec"
- group: System metrics (Kubernetes)
priority: 5
metrics:
......
......@@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do
project = Project.find_by_full_path('gitlab-org/gitlab-test')
next if project.empty_repo? # We don't have repository on CI
params = {
source_branch: 'feature',
target_branch: 'master',
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/SaferBooleanColumn
class AddKodingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/SaferBooleanColumn
class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
......@@ -9,9 +9,21 @@ class AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration
COLUMNS = %i[starts_at ends_at created_at updated_at message_html]
def change
class BroadcastMessage < ActiveRecord::Base
self.table_name = 'broadcast_messages'
end
def up
COLUMNS.each do |column|
BroadcastMessage.where(column => nil).delete_all
change_column_null :broadcast_messages, column, false
end
end
def down
COLUMNS.each do |column|
change_column_null :broadcast_messages, column, true
end
end
end
......@@ -69,3 +69,28 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE
[review-app]: ../review_apps/index.md
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/
## Auto Monitoring
> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438).
Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for:
* Response Metrics: latency, throughput, error rate
* System Metrics: CPU utilization, memory utilization
Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md).
To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments).
![Auto Metrics](img/auto_monitoring.png)
### Configuring Auto Monitoring
If GitLab has been deployed using the [omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no configuration is required.
If you have installed GitLab using a different method:
1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
......@@ -607,10 +607,9 @@ exist, you should see something like:
- With GitLab 9.2, all deployments to an environment are shown directly on the
monitoring dashboard
If you have enabled Prometheus for collecting metrics, you can monitor the performance behavior of your app
through the environments.
If you have enabled [Prometheus for monitoring system and response metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus.html), you can monitor the performance behavior of your app running in each environment.
Once configured, GitLab will attempt to retrieve performance metrics for any
Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any
environment which has had a successful deployment. If monitoring data was
successfully retrieved, a Monitoring button will appear on the environment's
detail page.
......
......@@ -511,7 +511,24 @@ A forEach will cause side effects, it will be mutating the array being iterated.
$('span').tooltip('fixTitle');
```
### The Javascript/Vue Accord
The goal of this accord is to make sure we are all on the same page.
1. When writing Vue, you may not use jQuery in your application.
1.1 If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery.
1.2 You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
1.3 If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners.
1.4 We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit).
1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application.
1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with it's priority to be determined by maintainers.
1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests.
1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
1. Once you have chosen a centralized state management solution you must use it for your entire application. i.e. Don't mix and match your state management solutions.
## SCSS
- [SCSS](style_guide_scss.md)
......
......@@ -279,6 +279,43 @@ end
- Avoid scenario titles that add no information, such as "successfully".
- Avoid scenario titles that repeat the feature title.
### Table-based / Parameterized tests
This style of testing is used to exercise one piece of code with a comprehensive
range of inputs. By specifying the test case once, alongside a table of inputs
and the expected output for each, your tests can be made easier to read and more
compact.
We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
gem. A short example, using the table syntax and checking Ruby equality for a
range of inputs, might look like this:
```ruby
describe "#==" do
using Rspec::Parameterized::TableSyntax
let(:project1) { create(:project) }
let(:project2) { create(:project) }
where(:a, :b, :result) do
1 | 1 | true
1 | 2 | false
true | true | true
true | false | false
project1 | project1 | true
project2 | project2 | true
project 1 | project2 | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
```
### Matchers
Custom matchers should be created to clarify the intent and/or hide the
......
......@@ -126,7 +126,7 @@ Let's Encrypt limits a single TLD to five certificate requests within a single w
## Installing GitLab using the Helm Chart
> You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically restart. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage.
Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab), you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) and [added the Helm repository](index.md#add-the-gitlab-helm-repository), you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
For example:
```bash
......
......@@ -153,6 +153,14 @@ Find this option under your project's settings.
GitLab administrators can use the admin interface to move any project to any namespace if needed.
## Sharing a project with a group
You can [share your projects with a group](../project/members/share_project_with_groups.md)
and give your group members access to the project all at once.
Alternatively, with [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
you can [lock the sharing with group feature](#share-with-group-lock-ees-eep).
## Manage group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
......@@ -189,7 +197,7 @@ Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#
In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing
a project with another group](../../workflow/share_projects_with_other_groups.md).
a project with another group](../project/members/share_project_with_groups.md).
This allows for tighter control over project access.
Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
......
......@@ -12,8 +12,8 @@ will be unassigned automatically.
GitLab administrators receive all permissions.
To add or import a user, you can follow the [project users and members
documentation](../workflow/add-user/add-user.md).
To add or import a user, you can follow the
[project members documentation](../user/project/members/index.md).
## Project
......
......@@ -98,7 +98,11 @@ from your fork to the upstream project
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md)
## Leave a project
## Project's members
Learn how to [add members to your projects](members/index.md).
### Leave a project
**Leave project** will only display on the project's dashboard
when a project is part of a group (under a
......
......@@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/
## Configuration
Each GitLab project can be configured to connect to a different JIRA instance.
Each GitLab project can be configured to connect to a different JIRA instance. That
means one GitLab project maps to _all_ JIRA projects in that JIRA instance once
the configuration is set up. Therefore, you don't have to explicitly associate
one GitLab project to any JIRA project. Once the configuration is set up, any JIRA
projects in the JIRA instance are already mapped to the GitLab project.
If you have one JIRA instance you can pre-fill the settings page with a default
template, see the [Services Templates][services-templates] docs.
......@@ -103,7 +108,6 @@ in the table below.
| ----- | ----------- |
| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
......
......@@ -40,7 +40,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments
With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled
version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>).
version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>).
1. Read how to configure the bundled Prometheus server in the
[Administration guide][gitlab-prometheus-k8s-monitor].
......@@ -133,6 +133,8 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment.
GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments)
## Determining the performance impact of a merge
......@@ -174,7 +176,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul
[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
[prometheus-yml]:samples/prometheus.yml
[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables
[ci-environment-slug]: ../../../ci/variables/#predefined-variables-environment-variables
[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
[promgldocs]: ../../../administration/monitoring/prometheus/index.md
......@@ -4,6 +4,7 @@
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are:
* [Kubernetes](kubernetes.md)
* [NGINX](nginx.md)
* [NGINX Ingress Controller](nginx_ingress.md)
* [HAProxy](haproxy.md)
* [Amazon Cloud Watch](cloudwatch.md)
......@@ -14,10 +15,7 @@ We have tried to surface the most important metrics for each exporter, and will
GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment.
In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that,
GitLab will look for the required metrics which have a label that
matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug].
For example if you are deploying to an environment named `production`, there must be a label for the metric with the value of `production`.
GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library).
## Adding to the library
......
......@@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro
| Name | Query |
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000 |
| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m])) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) |
| HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for NGINX metrics
......
# Monitoring NGINX Ingress Controller
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5
GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress.
## Metrics supported
| Name | Query |
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
## Configuring Prometheus to monitor for NGINX ingress metrics
The easiest way to get started is to use at least version 0.9.0 of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). If you are using NGINX as your Kubernetes ingress, there is [direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release.
If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, these metrics will be automatically enabled and annotated for Prometheus monitoring.
## Specifying the Environment label
In order to isolate and only display relevant metrics for a given environment
however, GitLab needs a method to detect which labels are associated. To do this, GitLab will search metrics with appropriate labels. In this case, the `upstream` label must be of the form `<Kubernetes Namespace>-<CI_ENVIRONMENT_SLUG>-*`.
If you have used [Auto Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part.
# Project's members
You can manage the groups and users and their access levels in all of your
projects. You can also personalize the access level you give each user,
per-project.
You should have `master` or `owner` [permissions](../../permissions.md) to add
or import a new user to your project.
To view, edit, add, and remove project's members, go to your
project's **Settings > Members**.
---
## Add a user
Right next to **People**, start typing the name or username of the user you
want to add.
![Search for people](img/add_user_search_people.png)
---
Select the user and the [permission level](../../user/permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
---
Once done, hit **Add users to project** and they will be immediately added to
your project with the permissions you gave them above.
![List members](img/add_user_list_members.png)
---
From there on, you can either remove an existing user or change their access
level to the project.
## Import users from another project
You can import another project's users in your own project by hitting the
**Import members** button on the upper right corner of the **Members** menu.
In the dropdown menu, you can see only the projects you are Master on.
![Import members from another project](img/add_user_import_members_from_another_project.png)
---
Select the one you want and hit **Import project members**. A flash message
notifying you that the import was successful will appear, and the new members
are now in the project's members list. Notice that the permissions that they
had on the project you imported from are retained.
![Members list of new members](img/add_user_imported_members.png)
---
## Invite people using their e-mail address
If a user you want to give access to doesn't have an account on your GitLab
instance, you can invite them just by typing their e-mail address in the
user search field.
![Invite user by mail](img/add_user_email_search.png)
---
As you can imagine, you can mix inviting multiple people and adding existing
GitLab users to the project.
![Invite user by mail ready to submit](img/add_user_email_ready.png)
---
Once done, hit **Add users to project** and watch that there is a new member
with the e-mail address we used above. From there on, you can resend the
invitation, change their access level or even delete them.
![Invite user members list](img/add_user_email_accept.png)
---
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
## Request access to a project
As a project owner you can enable or disable non members to request access to
your project. Go to the project settings and click on **Allow users to request access**.
As a user, you can request to be a member of a project. Go to the project you'd
like to be a member of, and click the **Request Access** button on the right
side of your screen.
![Request access button](img/request_access_button.png)
---
Project owners & masters will be notified of your request and will be able to approve or
decline it on the members page.
![Manage access requests](img/access_requests_management.png)
---
If you change your mind before your request is approved, just click the
**Withdraw Access Request** button.
![Withdraw access request button](img/withdraw_access_request_button.png)
## Share project with group
Alternatively, you can [share a project with an entire group](share_project_with_groups.md) instead of adding users one by one.
# Share Projects with other Groups
You can share projects with other [groups](../../group/index.md). This makes it
possible to add a group of users to a project with a single action.
## Groups as collections of users
Groups are used primarily to [create collections of projects](../user/group/index.md), but you can also
take advantage of the fact that groups define collections of _users_, namely the group
members.
## Sharing a project with a group of users
The primary mechanism to give a group of users, say 'Engineering', access to a project,
say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
![The 'Groups' section in the project settings screen](img/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png)
## Maximum access level
!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](img/max_access_level.png)
In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
## Share project with group lock (EES/EEP)
In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing
a project with another group](../members/share_project_with_groups.md).
This allows for tighter control over project access.
Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
......@@ -20,6 +20,8 @@ documentation.
For security reasons, when using the command line, we strongly recommend
you to [connect with GitLab via SSH](../../../ssh/README.md).
## Files
## Create and edit files
Host your codebase in GitLab repositories by pushing your files to GitLab.
......@@ -47,6 +49,10 @@ it's easier to do so [via GitLab UI](web_editor.md):
To get started with the command line, please read through the
[command line basics documentation](../../../gitlab-basics/command-line-commands.md).
### Find files
Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository.
## Branches
When you submit changes in a new branch, you create a new version
......
......@@ -16,7 +16,7 @@ Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/is
## Project snippets
Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information.
Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information.
## Personal snippets
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册