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: []
|
||||
|
|
1
Gemfile
|
@ -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'
|
||||
|
|
33
Gemfile.lock
|
@ -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)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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);
|
||||
});
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
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>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
<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>
|
|
@ -1,5 +1,5 @@
|
|||
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>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<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
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
- 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) }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= 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) }),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- 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
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
- 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'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -1,10 +1,17 @@
|
|||
- 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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
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
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: move edit comment button outside of dropdown
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix timeouts when creating projects in groups with many members
|
||||
merge_request: 13508
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Prevents jobs dropdown from closing in pipeline graph
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix edit merge request and issues button inconsistent letter casing
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cache the number of forks of a project
|
||||
merge_request: 13535
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a `Last 7 days` option for Cycle Analytics view
|
||||
merge_request: 13443
|
||||
author: Mehdi Lahmam (@mehlah)
|
||||
type: added
|
|
@ -1,3 +1,33 @@
|
|||
- 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',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# 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
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# rubocop:disable Migration/SaferBooleanColumn
|
||||
class AddKodingToApplicationSettings < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# 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
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# 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
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# 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
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# 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
|
||||
|
|
After Width: | Height: | Size: 87 KiB |
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 189 KiB |
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# 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.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,116 @@
|
|||
# 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.
|
|
@ -0,0 +1,41 @@
|
|||
# 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
|
||||
|
||||
|
|