Commit 35659da1 authored by Clement Ho's avatar Clement Ho

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

# Conflicts:
#   app/views/projects/merge_requests/_nav_btns.html.haml
parents 1405bf84 fe09c25d
...@@ -226,6 +226,7 @@ update-tests-metadata: ...@@ -226,6 +226,7 @@ update-tests-metadata:
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine image: ruby:2.3-alpine
services: [] services: []
before_script: [] before_script: []
......
...@@ -324,6 +324,7 @@ group :development, :test do ...@@ -324,6 +324,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -2,6 +2,7 @@ GEM ...@@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2) ace-rails-ap (4.1.2)
actionmailer (4.2.8) actionmailer (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
...@@ -41,6 +42,9 @@ GEM ...@@ -41,6 +42,9 @@ GEM
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0) acts-as-taggable-on (4.0.0)
activerecord (>= 4.0) activerecord (>= 4.0)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -124,6 +128,9 @@ GEM ...@@ -124,6 +128,9 @@ GEM
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
colorize (0.7.7) colorize (0.7.7)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5) concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5) concurrent-ruby (= 1.0.5)
...@@ -470,6 +477,8 @@ GEM ...@@ -470,6 +477,8 @@ GEM
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.15.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
...@@ -610,6 +619,11 @@ GEM ...@@ -610,6 +619,11 @@ GEM
premailer-rails (1.9.7) premailer-rails (1.9.7)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) 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) prometheus-client-mmap (0.7.0.beta11)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
...@@ -718,6 +732,10 @@ GEM ...@@ -718,6 +732,10 @@ GEM
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2) 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-core (3.6.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-expectations (3.6.0) rspec-expectations (3.6.0)
...@@ -726,6 +744,12 @@ GEM ...@@ -726,6 +744,12 @@ GEM
rspec-mocks (3.6.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.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) rspec-rails (3.6.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
...@@ -892,6 +916,14 @@ GEM ...@@ -892,6 +916,14 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.10.0) 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) url_safe_base64 (0.2.2)
validates_hostname (1.0.6) validates_hostname (1.0.6)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -1094,6 +1126,7 @@ DEPENDENCIES ...@@ -1094,6 +1126,7 @@ DEPENDENCIES
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
......
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const form = $('.commits-search-form');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({ $.get({
url: form.data('signatures-path'), url: form.data('signatures-path'),
data: form.serialize(), data: form.serialize(),
}).done((response) => { }).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => { response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); 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 Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout', el: '#pipeline-schedules-callout',
......
...@@ -48,6 +48,27 @@ ...@@ -48,6 +48,27 @@
return `${this.job.name} - ${this.job.status.label}`; 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> </script>
<template> <template>
......
...@@ -29,12 +29,10 @@ export default { ...@@ -29,12 +29,10 @@ export default {
editMode() { editMode() {
if (this.editMode) { if (this.editMode) {
$('.project-refs-form').addClass('disabled'); $('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show(); $('.js-tree-ref-target-holder').show();
$('.project-refs-target-form').show();
} else { } else {
$('.project-refs-form').removeClass('disabled'); $('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide(); $('.js-tree-ref-target-holder').hide();
$('.project-refs-target-form').hide();
} }
}, },
}, },
......
...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store'; ...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() { mounted() {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}, },
computed: { computed: {
html() { html() {
...@@ -12,10 +12,16 @@ export default { ...@@ -12,10 +12,16 @@ export default {
}, },
}, },
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}); });
}, },
}, },
...@@ -24,9 +30,23 @@ export default { ...@@ -24,9 +30,23 @@ export default {
<template> <template>
<div> <div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div> <div
<div v-if="activeFile.render_error" class="vertical-center render-error"> v-if="!activeFile.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> 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>
</div> </div>
</template> </template>
...@@ -33,32 +33,30 @@ const RepoSidebar = { ...@@ -33,32 +33,30 @@ const RepoSidebar = {
}); });
}, },
linkClicked(clickedFile) { fileClicked(clickedFile) {
let url = '';
let file = clickedFile; let file = clickedFile;
if (typeof file === 'object') {
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
url = file.url; Service.url = file.url;
Service.url = url; Helper.getContent(file)
// I need to refactor this to do the `then` here. .then(() => {
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false; file.loading = false;
Helper.scrollTabsRight(); Helper.scrollTabsRight();
}); })
} .catch(Helper.loadingError);
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
} }
}, },
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
}, },
}; };
...@@ -82,7 +80,7 @@ export default RepoSidebar; ...@@ -82,7 +80,7 @@ export default RepoSidebar;
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="isRoot"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/> @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file <repo-loading-file
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
...@@ -94,7 +92,7 @@ export default RepoSidebar; ...@@ -94,7 +92,7 @@ export default RepoSidebar;
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini" :is-mini="isMini"
@linkclicked="linkClicked(file)" @linkclicked="fileClicked(file)"
:is-tree="isTree" :is-tree="isTree"
:has-files="!!files.length" :has-files="!!files.length"
:active-file="activeFile"/> :active-file="activeFile"/>
......
...@@ -10,6 +10,12 @@ const RepoTab = { ...@@ -10,6 +10,12 @@ const RepoTab = {
}, },
computed: { computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times': !this.tab.changed, 'fa-times': !this.tab.changed,
...@@ -34,12 +40,24 @@ export default RepoTab; ...@@ -34,12 +40,24 @@ export default RepoTab;
<template> <template>
<li> <li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> <a
<i class="fa" :class="changedClass"></i> href="#0"
class="close"
@click.prevent="xClicked(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a> </a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> <a
href="#"
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li> </li>
</template> </template>
<script> <script>
import Vue from 'vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
...@@ -14,29 +13,19 @@ const RepoTabs = { ...@@ -14,29 +13,19 @@ const RepoTabs = {
data: () => Store, data: () => Store,
methods: { methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) { xClicked(file) {
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
}, },
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
}; };
export default RepoTabs; export default RepoTabs;
</script> </script>
<template> <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"/> <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" /> <li class="tabs-divider" />
</ul> </ul>
......
...@@ -10,7 +10,10 @@ function repoEditorLoader() { ...@@ -10,7 +10,10 @@ function repoEditorLoader() {
Store.monaco = monaco; Store.monaco = monaco;
Store.monacoLoading = false; Store.monacoLoading = false;
resolve(RepoEditor); resolve(RepoEditor);
}, reject); }, () => {
Store.monacoLoading = false;
reject();
});
}); });
} }
......
...@@ -33,12 +33,16 @@ const RepoHelper = { ...@@ -33,12 +33,16 @@ const RepoHelper = {
? window.performance ? window.performance
: Date, : Date,
getFileExtension(fileName) {
return fileName.split('.').pop();
},
getBranch() { getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref'); return $('button.dropdown-menu-toggle').attr('data-ref');
}, },
getLanguageIDForFile(file, langs) { getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop(); const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs); const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext'; return foundLang ? foundLang.id : 'plaintext';
...@@ -135,21 +139,19 @@ const RepoHelper = { ...@@ -135,21 +139,19 @@ const RepoHelper = {
return isRoot; return isRoot;
}, },
getContent(treeOrFile, cb) { getContent(treeOrFile) {
let file = treeOrFile; let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true); // const loadingData = RepoHelper.setLoading(true);
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
// RepoHelper.setLoading(false, loadingData); // RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
if (data.binary) { if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
...@@ -188,9 +190,8 @@ const RepoHelper = { ...@@ -188,9 +190,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url; newFile.url = file.url;
if (newFile.render_error === 'too_large') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
newFile.newContent = ''; newFile.newContent = '';
...@@ -199,10 +200,6 @@ const RepoHelper = { ...@@ -199,10 +200,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) { serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message; simpleBlob.lastCommitMessage = blob.last_commit.message;
...@@ -226,7 +223,7 @@ const RepoHelper = { ...@@ -226,7 +223,7 @@ const RepoHelper = {
type, type,
name, name,
url, url,
icon: RepoHelper.toFA(icon), icon: `fa-${icon}`,
level: 0, level: 0,
loading: false, loading: false,
}; };
...@@ -244,7 +241,7 @@ const RepoHelper = { ...@@ -244,7 +241,7 @@ const RepoHelper = {
setTimeout(() => { setTimeout(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
if (!tabs) return; if (!tabs) return;
tabs.scrollLeft = 12000; tabs.scrollLeft = tabs.scrollWidth;
}, 200); }, 200);
}, },
......
...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue'; ...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
$('.project-refs-target-form').hide(); $('.js-tree-ref-target-holder').hide();
$('.fa-long-arrow-right').hide();
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = { const RepoService = {
url: '', url: '',
...@@ -22,6 +23,7 @@ const RepoService = { ...@@ -22,6 +23,7 @@ const RepoService = {
getRaw(url) { getRaw(url) {
return axios.get(url, { return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
}, },
...@@ -36,7 +38,7 @@ const RepoService = { ...@@ -36,7 +38,7 @@ const RepoService = {
}, },
urlIsRichBlob(url = this.url) { urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop(); const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension); return this.richExtensionRegExp.test(extension);
}, },
......
...@@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper'; ...@@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
ideEl: {},
monaco: {}, monaco: {},
monacoLoading: false, monacoLoading: false,
monacoInstance: {}, monacoInstance: {},
service: '', service: '',
editor: '',
sidebar: '',
editMode: false, editMode: false,
isTree: false, isTree: false,
isRoot: false, isRoot: false,
...@@ -17,19 +14,10 @@ const RepoStore = { ...@@ -17,19 +14,10 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '', blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false, submitCommitsLoading: false,
binaryLoaded: false,
dialog: { dialog: {
open: false, open: false,
title: '', title: '',
...@@ -45,9 +33,6 @@ const RepoStore = { ...@@ -45,9 +33,6 @@ const RepoStore = {
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch', targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: { binaryTypes: {
png: false, png: false,
md: false, md: false,
...@@ -58,7 +43,6 @@ const RepoStore = { ...@@ -58,7 +43,6 @@ const RepoStore = {
tree: false, tree: false,
blob: false, blob: false,
}, },
readOnly: true,
resetBinaryTypes() { resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => { Object.keys(RepoStore.binaryTypes).forEach((key) => {
...@@ -96,7 +80,6 @@ const RepoStore = { ...@@ -96,7 +80,6 @@ const RepoStore = {
if (file.binary) { if (file.binary) {
RepoStore.blobRaw = file.base64; RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
...@@ -238,4 +221,5 @@ const RepoStore = { ...@@ -238,4 +221,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview'; return RepoStore.currentBlobView === 'repo-preview';
}, },
}; };
export default RepoStore; export default RepoStore;
...@@ -71,7 +71,7 @@ export default { ...@@ -71,7 +71,7 @@ export default {
/> />
<div v-if="!isConfidential" class="no-value confidential-value"> <div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i> <i class="fa fa-eye is-not-confidential"></i>
None This issue is not confidential
</div> </div>
<div v-else class="value confidential-value hide-collapsed"> <div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
......
...@@ -286,6 +286,10 @@ ...@@ -286,6 +286,10 @@
.gpg-status-box { .gpg-status-box {
&:empty {
display: none;
}
&.valid { &.valid {
@include green-status-color; @include green-status-color;
} }
......
...@@ -453,7 +453,10 @@ ul.notes { ...@@ -453,7 +453,10 @@ ul.notes {
} }
.note-actions { .note-actions {
align-self: flex-start;
flex-shrink: 0; flex-shrink: 0;
display: inline-flex;
align-items: center;
// For PhantomJS that does not support flex // For PhantomJS that does not support flex
float: right; float: right;
margin-left: 10px; margin-left: 10px;
...@@ -463,18 +466,12 @@ ul.notes { ...@@ -463,18 +466,12 @@ ul.notes {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
} }
.more-actions { .more-actions {
display: inline-block; float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -482,16 +479,10 @@ ul.notes { ...@@ -482,16 +479,10 @@ ul.notes {
} }
.more-actions-toggle { .more-actions-toggle {
padding: 0;
&:hover .icon, &:hover .icon,
&:focus .icon { &:focus .icon {
color: $blue-600; color: $blue-600;
} }
.icon {
padding: 0 6px;
}
} }
.more-actions-dropdown { .more-actions-dropdown {
...@@ -519,28 +510,42 @@ ul.notes { ...@@ -519,28 +510,42 @@ ul.notes {
@include notes-media('max', $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
}
}
.note-action-button { .note-actions-item {
margin-left: 0; margin-left: 15px;
} display: flex;
align-items: center;
&.more-actions {
// compensate for narrow icon
margin-left: 10px;
} }
} }
.note-action-button { .note-action-button {
display: inline; line-height: 1;
line-height: 20px; padding: 0;
min-width: 16px;
color: $gray-darkest;
.fa { .fa {
color: $gray-darkest;
position: relative; position: relative;
font-size: 17px; font-size: 16px;
} }
svg { svg {
height: 16px; height: 16px;
width: 16px; width: 16px;
fill: $gray-darkest; top: 0;
vertical-align: text-top; vertical-align: text-top;
path {
fill: currentColor;
}
} }
.award-control-icon-positive, .award-control-icon-positive,
...@@ -613,10 +618,7 @@ ul.notes { ...@@ -613,10 +618,7 @@ ul.notes {
.note-role { .note-role {
position: relative; position: relative;
top: -2px; padding: 0 7px;
display: inline-block;
padding-left: 7px;
padding-right: 7px;
color: $notes-role-color; color: $notes-role-color;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
......
...@@ -29,6 +29,10 @@ ...@@ -29,6 +29,10 @@
margin-right: 15px; margin-right: 15px;
} }
.tree-ref-target-holder {
display: inline-block;
}
.repo-breadcrumb { .repo-breadcrumb {
li:last-of-type { li:last-of-type {
position: relative; position: relative;
......
...@@ -6,6 +6,13 @@ module CycleAnalyticsParams ...@@ -6,6 +6,13 @@ module CycleAnalyticsParams
end end
def start_date(params) 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
end end
...@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create_merge_request 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 if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
...@@ -59,7 +59,7 @@ module GroupsHelper ...@@ -59,7 +59,7 @@ module GroupsHelper
end end
def remove_group_message(group) 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 } { group_name: group.name }
end end
......
...@@ -80,7 +80,7 @@ module ProjectsHelper ...@@ -80,7 +80,7 @@ module ProjectsHelper
end end
def remove_project_message(project) 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 } { project_name_with_namespace: project.name_with_namespace }
end end
......
...@@ -11,11 +11,11 @@ module Emails ...@@ -11,11 +11,11 @@ module Emails
@member_source_type = member_source_type @member_source_type = member_source_type
@member_id = member_id @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 # A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters. # we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group 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 end
mail(to: admins, mail(to: admins,
......
...@@ -212,21 +212,39 @@ class Group < Namespace ...@@ -212,21 +212,39 @@ class Group < Namespace
end end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
users_with_parents.pluck(:id) members_with_parents.pluck(:user_id)
end end
def members_with_parents 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 end
def users_with_parents 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 end
def users_with_descendants 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 end
def max_member_access_for_user(user) def max_member_access_for_user(user)
......
...@@ -41,9 +41,20 @@ class Member < ActiveRecord::Base ...@@ -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)) 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) user_is_active = User.arel_table[:state].eq(:active)
includes(:user).references(:users) user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
.where(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) .where(requested_at: nil)
.reorder(nil)
end end
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
......
...@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
end end
def reload_diff_if_branch_changed 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 reload_diff
end end
end end
...@@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base
end end
def fetch_ref def fetch_ref
target_project.repository.fetch_ref( write_ref
source_project.repository.path_to_repo,
"refs/heads/#{source_branch}",
ref_path
)
update_column(:ref_fetched, true) update_column(:ref_fetched, true)
end end
...@@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base ...@@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base
true true
end 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 end
...@@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base ...@@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors .base_and_ancestors
end 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. # Returns all the descendants of the current namespace.
def descendants def descendants
Gitlab::GroupHierarchy Gitlab::GroupHierarchy
...@@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base ...@@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base
.base_and_descendants .base_and_descendants
end end
def self_and_descendants
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_descendants
end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
[owner_id] [owner_id]
end end
......
...@@ -196,7 +196,6 @@ class Project < ActiveRecord::Base ...@@ -196,7 +196,6 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
...@@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base ...@@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base
def change_head(branch) def change_head(branch)
if repository.branch_exists?(branch) if repository.branch_exists?(branch)
repository.before_change_head repository.before_change_head
repository.rugged.references.create('HEAD', repository.write_ref('HEAD', "refs/heads/#{branch}")
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.after_change_head repository.after_change_head
reload_default_branch reload_default_branch
...@@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base ...@@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base
# @deprecated cannot remove yet because it has an index with its name in elasticsearch # @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path alias_method :path_with_namespace, :full_path
def forks_count
Projects::ForksCountService.new(self).count
end
private private
def cross_namespace_reference?(from) def cross_namespace_reference?(from)
......
...@@ -224,7 +224,7 @@ class Repository ...@@ -224,7 +224,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes) # This will still fail if the file is corrupted (e.g. 0 bytes)
begin begin
rugged.references.create(keep_around_ref_name(sha), sha, force: true) write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex rescue Rugged::OSError => ex
...@@ -237,6 +237,10 @@ class Repository ...@@ -237,6 +237,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
...@@ -985,12 +989,10 @@ class Repository ...@@ -985,12 +989,10 @@ class Repository
if start_repository == self if start_repository == self
start_branch_name start_branch_name
else else
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" tmp_ref = fetch_ref(
fetch_ref(
start_repository.path_to_repo, start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
tmp_ref "refs/tmp/#{SecureRandom.hex}/head"
) )
start_repository.commit(start_branch_name).sha start_repository.commit(start_branch_name).sha
...@@ -1021,7 +1023,12 @@ class Repository ...@@ -1021,7 +1023,12 @@ class Repository
def fetch_ref(source_path, source_ref, target_ref) def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{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 end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)
......
...@@ -726,9 +726,9 @@ class User < ActiveRecord::Base ...@@ -726,9 +726,9 @@ class User < ActiveRecord::Base
end end
def sanitize_attrs def sanitize_attrs
%w[username skype linkedin twitter].each do |attr| %i[skype linkedin twitter].each do |attr|
value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend value = self[attr]
public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend self[attr] = Sanitize.clean(value) if value.present?
end end
end end
......
...@@ -128,6 +128,8 @@ module Projects ...@@ -128,6 +128,8 @@ module Projects
project.repository.before_delete project.repository.before_delete
Repository.new(wiki_path, project, disk_path: repo_path).before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete
Projects::ForksCountService.new(project).delete_cache
end end
end end
end end
...@@ -21,11 +21,17 @@ module Projects ...@@ -21,11 +21,17 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
new_project new_project
end end
private private
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
def allowed_visibility_level def allowed_visibility_level
project_level = @project.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 ...@@ -13,7 +13,13 @@ module Projects
::MergeRequests::CloseService.new(@project, @current_user).execute(mr) ::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end end
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy @project.forked_project_link.destroy
end end
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
end end
end end
- if commit.has_signature? - 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 } } %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 @@ ...@@ -39,6 +39,9 @@
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
%li %li
%a{ "href" => "#", "data-value" => "30" } %a{ "href" => "#", "data-value" => "30" }
{{ n__('Last %d day', 'Last %d days', 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 = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss') = icon('rss')
- if @can_bulk_update - 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, = link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: issues_finder.assignee.try(:id), issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }), milestone_id: issues_finder.milestones.first.try(:id) }),
......
- if @can_bulk_update - 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 - if merge_project
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
New merge request New merge request
...@@ -17,24 +17,32 @@ ...@@ -17,24 +17,32 @@
"inline-template" => true, "inline-template" => true,
"ref" => "note_#{note.id}" } "ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button", .note-actions-item
class: ("is-disabled" unless can_resolve), %button.note-action-button.line-resolve-btn{ type: "button",
":class" => "{ 'is-active': isResolved }", class: ("is-disabled" unless can_resolve),
":aria-label" => "buttonText", ":class" => "{ 'is-active': isResolved }",
"@click" => "resolve", ":aria-label" => "buttonText",
":title" => "buttonText", "@click" => "resolve",
":ref" => "'button'" } ":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') = 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' %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
- user_authored = note.user_authored?(current_user) - 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 .note-actions-item
= icon('spinner spin') = 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
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') = icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') %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 - is_current_user = current_user == note.author
- if note_editable || !is_current_user - 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 = 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 %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 - unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo? - if show_new_repo?
= icon('long-arrow-right', title: 'to target branch') .tree-ref-target-holder.js-tree-ref-target-holder
= render 'shared/target_switcher', destination: 'tree', path: @path = icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path
- unless show_new_repo? - unless show_new_repo?
= render 'projects/tree/old_tree_header' = 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 current_user
- if note.emoji_awardable? - if note.emoji_awardable?
- user_authored = note.user_authored?(current_user) - 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 .note-actions-item
= icon('spinner spin') = 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
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') = icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') %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 = 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) - group: Response metrics (HA Proxy)
priority: 10 priority: 10
metrics: metrics:
...@@ -68,18 +98,18 @@ ...@@ -68,18 +98,18 @@
- nginx_upstream_response_msecs_avg - nginx_upstream_response_msecs_avg
weight: 1 weight: 1
queries: queries:
- query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000' - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})'
label: Upstream label: Upstream
unit: ms unit: ms
- title: "HTTP Error Rate" - title: "HTTP Error Rate"
y_label: "Error Rate (%)" y_label: "HTTP 500 Errors / Sec"
required_metrics: required_metrics:
- nginx_responses_total - nginx_responses_total
weight: 1 weight: 1
queries: 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 label: HTTP Errors
unit: "%" unit: "errors / sec"
- group: System metrics (Kubernetes) - group: System metrics (Kubernetes)
priority: 5 priority: 5
metrics: metrics:
......
...@@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do ...@@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do
project = Project.find_by_full_path('gitlab-org/gitlab-test') project = Project.find_by_full_path('gitlab-org/gitlab-test')
next if project.empty_repo? # We don't have repository on CI
params = { params = {
source_branch: 'feature', source_branch: 'feature',
target_branch: 'master', target_branch: 'master',
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/SaferBooleanColumn
class AddKodingToApplicationSettings < ActiveRecord::Migration class AddKodingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/SaferBooleanColumn
class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
...@@ -9,9 +9,21 @@ class AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration ...@@ -9,9 +9,21 @@ class AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration
COLUMNS = %i[starts_at ends_at created_at updated_at message_html] 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| COLUMNS.each do |column|
BroadcastMessage.where(column => nil).delete_all
change_column_null :broadcast_messages, column, false change_column_null :broadcast_messages, column, false
end end
end end
def down
COLUMNS.each do |column|
change_column_null :broadcast_messages, column, true
end
end
end end
...@@ -69,3 +69,28 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE ...@@ -69,3 +69,28 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE
[review-app]: ../review_apps/index.md [review-app]: ../review_apps/index.md
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html [container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/ [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: ...@@ -607,10 +607,9 @@ exist, you should see something like:
- With GitLab 9.2, all deployments to an environment are shown directly on the - With GitLab 9.2, all deployments to an environment are shown directly on the
monitoring dashboard monitoring dashboard
If you have enabled Prometheus for collecting metrics, you can monitor the performance behavior of your app 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.
through the environments.
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 environment which has had a successful deployment. If monitoring data was
successfully retrieved, a Monitoring button will appear on the environment's successfully retrieved, a Monitoring button will appear on the environment's
detail page. detail page.
......
...@@ -511,7 +511,24 @@ A forEach will cause side effects, it will be mutating the array being iterated. ...@@ -511,7 +511,24 @@ A forEach will cause side effects, it will be mutating the array being iterated.
$('span').tooltip('fixTitle'); $('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
- [SCSS](style_guide_scss.md) - [SCSS](style_guide_scss.md)
......
...@@ -279,6 +279,43 @@ end ...@@ -279,6 +279,43 @@ end
- Avoid scenario titles that add no information, such as "successfully". - Avoid scenario titles that add no information, such as "successfully".
- Avoid scenario titles that repeat the feature title. - 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 ### Matchers
Custom matchers should be created to clarify the intent and/or hide the 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 ...@@ -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 ## 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. > 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: For example:
```bash ```bash
......
...@@ -153,6 +153,14 @@ Find this option under your project's settings. ...@@ -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. 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 ## Manage group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. 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# ...@@ -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/) In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing 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. 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). 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. ...@@ -12,8 +12,8 @@ will be unassigned automatically.
GitLab administrators receive all permissions. GitLab administrators receive all permissions.
To add or import a user, you can follow the [project users and members To add or import a user, you can follow the
documentation](../workflow/add-user/add-user.md). [project members documentation](../user/project/members/index.md).
## Project ## Project
......
...@@ -98,7 +98,11 @@ from your fork to the upstream 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) - [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) - [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 **Leave project** will only display on the project's dashboard
when a project is part of a group (under a 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/ ...@@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/
## Configuration ## 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 If you have one JIRA instance you can pre-fill the settings page with a default
template, see the [Services Templates][services-templates] docs. template, see the [Services Templates][services-templates] docs.
...@@ -103,7 +108,6 @@ in the table below. ...@@ -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`. | | `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`. | | `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). | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user 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.** | | `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 ...@@ -40,7 +40,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments ### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments
With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled 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 1. Read how to configure the bundled Prometheus server in the
[Administration guide][gitlab-prometheus-k8s-monitor]. [Administration guide][gitlab-prometheus-k8s-monitor].
...@@ -133,6 +133,8 @@ to integrate with. ...@@ -133,6 +133,8 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment. 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) [Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments)
## Determining the performance impact of a merge ## Determining the performance impact of a merge
...@@ -174,7 +176,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul ...@@ -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-docker-image]: https://hub.docker.com/r/prom/prometheus/
[prometheus-yml]:samples/prometheus.yml [prometheus-yml]:samples/prometheus.yml
[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434 [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-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408 [ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
[promgldocs]: ../../../administration/monitoring/prometheus/index.md [promgldocs]: ../../../administration/monitoring/prometheus/index.md
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are: GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are:
* [Kubernetes](kubernetes.md) * [Kubernetes](kubernetes.md)
* [NGINX](nginx.md) * [NGINX](nginx.md)
* [NGINX Ingress Controller](nginx_ingress.md)
* [HAProxy](haproxy.md) * [HAProxy](haproxy.md)
* [Amazon Cloud Watch](cloudwatch.md) * [Amazon Cloud Watch](cloudwatch.md)
...@@ -14,10 +15,7 @@ We have tried to surface the most important metrics for each exporter, and will ...@@ -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. 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, 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 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).
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`.
## Adding to the library ## Adding to the library
......
...@@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro ...@@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro
| Name | Query | | Name | Query |
| ---- | ----- | | ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) | | 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 | | Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) |
| 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])) | | HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for NGINX metrics ## 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. ...@@ -20,6 +20,8 @@ documentation.
For security reasons, when using the command line, we strongly recommend For security reasons, when using the command line, we strongly recommend
you to [connect with GitLab via SSH](../../../ssh/README.md). you to [connect with GitLab via SSH](../../../ssh/README.md).
## Files
## Create and edit files ## Create and edit files
Host your codebase in GitLab repositories by pushing your files to GitLab. 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): ...@@ -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 To get started with the command line, please read through the
[command line basics documentation](../../../gitlab-basics/command-line-commands.md). [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 ## Branches
When you submit changes in a new branch, you create a new version 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 ...@@ -16,7 +16,7 @@ Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/is
## Project snippets ## 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 ## Personal snippets
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment