Commit b43cabaf authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into sh-fix-issue-31215

* upstream/master: (109 commits)
  Update CI templates to include 9.1 templates
  Change spec folder to match the assets one
  Update style_guide_js.md
  Milestones documentation refactor
  Adds documentation entry: Don't user forEach, aim for code without side effects
  Move kube namespace section to the variables one
  Changed milestone.to_reference calls into milestone.title for the show, edit and top views
  Update move icon to match others
  Issue Title Show Focus Check On Load
  Update Kubernetes namespace documentation
  Store projects in metrics for email replies
  Refactor into .vue files
  Adds vue js example application and documentation
  Add ES lint support to identify poorly written Promises
  Update plantuml.md to add the actual link.
  Fixed wording
  Add metrics events for incoming emails
  Remove helpers assigned_issuables_count and cached_assigned_issuables_count
  Refactor into .vue files part 2
  Fix headings
  ...
parents 8a570944 4b379615
...@@ -14,7 +14,8 @@ ...@@ -14,7 +14,8 @@
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
"html" "html",
"promise"
], ],
"settings": { "settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"], "html/html-extensions": [".html", ".html.raw", ".vue"],
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
}, },
"rules": { "rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+$"], "filenames/match-regex": [2, "^[a-z0-9_]+$"],
"no-multiple-empty-lines": ["error", { "max": 1 }] "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error"
} }
} }
...@@ -201,7 +201,13 @@ rake config_lint: *exec ...@@ -201,7 +201,13 @@ rake config_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flay: *exec rake flay: *exec
license_finder: *exec license_finder: *exec
rake downtime_check: *exec rake downtime_check:
<<: *exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
rake ee_compat_check: rake ee_compat_check:
<<: *exec <<: *exec
only: only:
...@@ -278,7 +284,6 @@ rake karma: ...@@ -278,7 +284,6 @@ rake karma:
cache: cache:
paths: paths:
- vendor/ruby - vendor/ruby
- node_modules
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner <<: *dedicated-runner
...@@ -377,9 +382,6 @@ coverage: ...@@ -377,9 +382,6 @@ coverage:
lint:javascript: lint:javascript:
<<: *dedicated-runner <<: *dedicated-runner
cache:
paths:
- node_modules/
stage: test stage: test
before_script: [] before_script: []
script: script:
...@@ -387,9 +389,6 @@ lint:javascript: ...@@ -387,9 +389,6 @@ lint:javascript:
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
cache:
paths:
- node_modules/
stage: post-test stage: post-test
before_script: [] before_script: []
script: script:
......
...@@ -239,6 +239,9 @@ AwardsHandler ...@@ -239,6 +239,9 @@ AwardsHandler
if (menu) { if (menu) {
menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish')); menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
} }
}).catch((err) => {
emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
}); });
}; };
......
...@@ -35,7 +35,7 @@ export default class BlobFileDropzone { ...@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { this.on('sending', function (file, xhr, formData) {
formData.append('target_branch', form.find('input[name="target_branch"]').val()); formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val()); formData.append('commit_message', form.find('.js-commit-message').val());
}); });
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */ /* global BoardService */
/* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
...@@ -93,7 +94,7 @@ $(() => { ...@@ -93,7 +94,7 @@ $(() => {
Store.addBlankState(); Store.addBlankState();
this.loading = false; this.loading = false;
}); }).catch(() => new Flash('An error occurred. Please try again.'));
}, },
methods: { methods: {
updateTokens() { updateTokens() {
......
...@@ -57,12 +57,15 @@ export default { ...@@ -57,12 +57,15 @@ export default {
}, },
loadNextPage() { loadNextPage() {
const getIssues = this.list.nextPage(); const getIssues = this.list.nextPage();
const loadingDone = () => {
this.list.loadingMore = false;
};
if (getIssues) { if (getIssues) {
this.list.loadingMore = true; this.list.loadingMore = true;
getIssues.then(() => { getIssues
this.list.loadingMore = false; .then(loadingDone)
}); .catch(loadingDone);
} }
}, },
toggleForm() { toggleForm() {
......
...@@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
showAddIssuesModal() { showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) { if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true; this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues() this.loadIssues()
.then(() => { .then(loadingDone)
this.loading = false; .catch(loadingDone);
});
} else if (!this.showAddIssuesModal) { } else if (!this.showAddIssuesModal) {
this.issues = []; this.issues = [];
this.selectedIssues = []; this.selectedIssues = [];
...@@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
if (this.$el.tagName) { if (this.$el.tagName) {
this.page = 1; this.page = 1;
this.filterLoading = true; this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true) this.loadIssues(true)
.then(() => { .then(loadingDone)
this.filterLoading = false; .catch(loadingDone);
});
} }
}, },
deep: true, deep: true,
......
/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
......
...@@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = { ...@@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = {
.save() .save()
.then(() => { .then(() => {
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
})
.catch(() => {
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
}); });
this.removeBlankState(); this.removeBlankState();
}, },
......
...@@ -64,6 +64,8 @@ const ResolveBtn = Vue.extend({ ...@@ -64,6 +64,8 @@ const ResolveBtn = Vue.extend({
}); });
}, },
resolve: function () { resolve: function () {
const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.';
if (!this.canResolve) return; if (!this.canResolve) return;
let promise; let promise;
...@@ -87,10 +89,12 @@ const ResolveBtn = Vue.extend({ ...@@ -87,10 +89,12 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data); this.discussion.updateHeadline(data);
} else { } else {
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); new Flash(errorFlashMsg);
} }
this.updateTooltip(); this.updateTooltip();
}).catch(() => {
new Flash(errorFlashMsg);
}); });
} }
}, },
......
...@@ -51,8 +51,10 @@ class ResolveServiceClass { ...@@ -51,8 +51,10 @@ class ResolveServiceClass {
discussion.updateHeadline(data); discussion.updateHeadline(data);
} else { } else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); throw new Error('An error occurred when trying to resolve discussion.');
} }
}).catch(() => {
new Flash('An error occurred when trying to resolve a discussion. Please try again.');
}); });
} }
......
...@@ -150,13 +150,13 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -150,13 +150,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
case 'projects:milestones:update': case 'projects:milestones:update':
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode(); new ZenMode();
new gl.DueDateSelectors(); new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form')); new gl.GLForm($('.milestone-form'));
break; break;
case 'groups:milestones:new':
new ZenMode();
break;
case 'projects:compare:show': case 'projects:compare:show':
new gl.Diff(); new gl.Diff();
break; break;
...@@ -367,6 +367,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -367,6 +367,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'admin': case 'admin':
new Admin(); new Admin();
switch (path[1]) { switch (path[1]) {
case 'cohorts':
new gl.UsagePing();
break;
case 'groups': case 'groups':
new UsersSelect(); new UsersSelect();
break; break;
......
...@@ -2,10 +2,12 @@ const DATA_TRIGGER = 'data-dropdown-trigger'; ...@@ -2,10 +2,12 @@ const DATA_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN = 'data-dropdown'; const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected'; const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active'; const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
export { export {
DATA_TRIGGER, DATA_TRIGGER,
DATA_DROPDOWN, DATA_DROPDOWN,
SELECTED_CLASS, SELECTED_CLASS,
ACTIVE_CLASS, ACTIVE_CLASS,
IGNORE_CLASS,
}; };
/* eslint-disable */ /* eslint-disable */
import utils from './utils'; import utils from './utils';
import { SELECTED_CLASS } from './constants'; import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
var DropDown = function(list) { var DropDown = function(list) {
this.currentIndex = 0; this.currentIndex = 0;
...@@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, { ...@@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, {
clickEvent: function(e) { clickEvent: function(e) {
if (e.target.tagName === 'UL') return; if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return;
var selected = utils.closest(e.target, 'LI'); var selected = utils.closest(e.target, 'LI');
if (!selected) return; if (!selected) return;
......
...@@ -38,6 +38,9 @@ window.DropzoneInput = (function() { ...@@ -38,6 +38,9 @@ window.DropzoneInput = (function() {
"opacity": 0, "opacity": 0,
"display": "none" "display": "none"
}); });
if (!project_uploads_path) return;
dropzone = form_dropzone.dropzone({ dropzone = form_dropzone.dropzone({
url: project_uploads_path, url: project_uploads_path,
dictDefaultMessage: "", dictDefaultMessage: "",
......
...@@ -115,11 +115,13 @@ class DueDateSelect { ...@@ -115,11 +115,13 @@ class DueDateSelect {
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide(); this.$selectbox.hide();
this.$value.css('display', ''); this.$value.css('display', '');
const fadeOutLoader = () => {
this.$loading.fadeOut();
};
gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
.then(() => { .then(fadeOutLoader)
this.$loading.fadeOut(); .catch(fadeOutLoader);
});
} }
submitSelectedDate(isDropdown) { submitSelectedDate(isDropdown) {
......
...@@ -35,6 +35,8 @@ export default { ...@@ -35,6 +35,8 @@ export default {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -62,6 +64,7 @@ export default { ...@@ -62,6 +64,7 @@ export default {
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body" data-container="body"
data-toggle="dropdown" data-toggle="dropdown"
ref="tooltip"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:disabled="isLoading"> :disabled="isLoading">
......
<script>
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
...@@ -5,7 +6,7 @@ export default { ...@@ -5,7 +6,7 @@ export default {
props: { props: {
externalUrl: { externalUrl: {
type: String, type: String,
default: '', required: true,
}, },
}, },
...@@ -14,17 +15,19 @@ export default { ...@@ -14,17 +15,19 @@ export default {
return 'Open'; return 'Open';
}, },
}, },
template: `
<a
class="btn external-url has-tooltip"
data-container="body"
:href="externalUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
}; };
</script>
<template>
<a
class="btn external-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title"
:href="externalUrl">
<i
class="fa fa-external-link"
aria-hidden="true" />
</a>
</template>
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import '../../lib/utils/text_utility'; import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions'; import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop'; import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
......
<script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
...@@ -5,7 +6,6 @@ export default { ...@@ -5,7 +6,6 @@ export default {
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
default: '',
required: true, required: true,
}, },
}, },
...@@ -15,17 +15,19 @@ export default { ...@@ -15,17 +15,19 @@ export default {
return 'Monitoring'; return 'Monitoring';
}, },
}, },
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
}; };
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title">
<i
class="fa fa-area-chart"
aria-hidden="true" />
</a>
</template>
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new */ /* eslint-disable no-new */
/** /**
...@@ -36,6 +37,8 @@ export default { ...@@ -36,6 +37,8 @@ export default {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl) this.service.postAction(this.retryUrl)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -47,21 +50,25 @@ export default { ...@@ -47,21 +50,25 @@ export default {
}); });
}, },
}, },
};
</script>
<template>
<button
type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
template: ` <span v-if="isLastDeployment">
<button type="button" Re-deploy
class="btn" </span>
@click="onClick" <span v-else>
:disabled="isLoading"> Rollback
</span>
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> <i
</button> v-if="isLoading"
`, class="fa fa-spinner fa-spin"
}; aria-hidden="true" />
</button>
</template>
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new, no-alert */ /* eslint-disable no-new, no-alert */
/** /**
...@@ -36,6 +37,8 @@ export default { ...@@ -36,6 +37,8 @@ export default {
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl) this.service.postAction(this.retryUrl)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -48,17 +51,23 @@ export default { ...@@ -48,17 +51,23 @@ export default {
} }
}, },
}, },
template: `
<button type="button"
class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick"
:disabled="isLoading"
:title="title"
:aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`,
}; };
</script>
<template>
<button
type="button"
class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick"
:disabled="isLoading"
:title="title"
:aria-label="title">
<i
class="fa fa-stop stop-env-icon"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</button>
</template>
<script>
/** /**
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
...@@ -24,14 +25,15 @@ export default { ...@@ -24,14 +25,15 @@ export default {
return 'Terminal'; return 'Terminal';
}, },
}, },
template: `
<a class="btn terminal-button has-tooltip"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
`,
}; };
</script>
<template>
<a
class="btn terminal-button has-tooltip"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"
v-html="terminalIconSvg">
</a>
</template>
...@@ -343,6 +343,8 @@ class FilteredSearchManager { ...@@ -343,6 +343,8 @@ class FilteredSearchManager {
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
this.recentSearchesService.save(resultantSearches); this.recentSearchesService.save(resultantSearches);
} }
}).catch(() => {
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
}); });
} }
......
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
camelcase, one-var-declaration-per-line, quotes, object-shorthand,
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
/* global Api */ /* global Api */
var slice = [].slice; var slice = [].slice;
......
...@@ -34,17 +34,6 @@ export default { ...@@ -34,17 +34,6 @@ export default {
}; };
}, },
methods: { methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) { renderResponse(res) {
const body = JSON.parse(res.body); const body = JSON.parse(res.body);
this.triggerAnimation(body); this.triggerAnimation(body);
...@@ -71,7 +60,17 @@ export default { ...@@ -71,7 +60,17 @@ export default {
}, },
}, },
created() { created() {
this.fetch(); if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}, },
}; };
</script> </script>
......
...@@ -332,6 +332,9 @@ ...@@ -332,6 +332,9 @@
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) { clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => {
$loading.fadeOut();
};
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
...@@ -396,9 +399,8 @@ ...@@ -396,9 +399,8 @@
$loading.fadeIn(); $loading.fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(fadeOutLoader)
$loading.fadeOut(); .catch(fadeOutLoader);
});
} }
else { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -165,6 +165,7 @@ import './syntax_highlight'; ...@@ -165,6 +165,7 @@ import './syntax_highlight';
import './task_list'; import './task_list';
import './todos'; import './todos';
import './tree'; import './tree';
import './usage_ping';
import './user'; import './user';
import './user_tabs'; import './user_tabs';
import './username_validator'; import './username_validator';
...@@ -210,6 +211,14 @@ $(function () { ...@@ -210,6 +211,14 @@ $(function () {
} }
}); });
if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar');
$rightSidebar
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
}
// prevent default action for disabled buttons // prevent default action for disabled buttons
$('.btn').click(function(e) { $('.btn').click(function(e) {
if ($(this).hasClass('disabled')) { if ($(this).hasClass('disabled')) {
......
...@@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
$('.ci-widget-fetching').show(); $('.ci-widget-fetching').show();
return $.getJSON(this.opts.ci_status_url, (function(_this) { return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) { return function(data) {
var message, status, title; var message, status, title, callback;
_this.status = data.status; _this.status = data.status;
_this.hasCi = data.has_ci; _this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi); _this.updateMergeButton(_this.status, _this.hasCi);
...@@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha; _this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha); _this.updateCommitUrls(data.sha);
} }
if (data.status === "success" || data.status === "failed") {
callback = function() {
return _this.getMergeStatus();
};
return setTimeout(callback, 2000);
}
if (showNotification && data.status) { if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
......
...@@ -164,6 +164,9 @@ ...@@ -164,6 +164,9 @@
.then(function () { .then(function () {
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut(); $loading.fadeOut();
})
.catch(() => {
$loading.fadeOut();
}); });
} else { } else {
selected = $selectbox.find('input[type="hidden"]').val(); selected = $selectbox.find('input[type="hidden"]').val();
......
...@@ -71,6 +71,8 @@ class PrometheusGraph { ...@@ -71,6 +71,8 @@ class PrometheusGraph {
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
} }
}).catch(() => {
new Flash('An error occurred when trying to load metrics. Please try again.');
}); });
} }
......
...@@ -308,8 +308,10 @@ require('./task_list'); ...@@ -308,8 +308,10 @@ require('./task_list');
if (this.isNewNote(note)) { if (this.isNewNote(note)) {
this.note_ids.push(note.id); this.note_ids.push(note.id);
$notesList = $('ul.main-notes-list');
$notesList.append(note.html).syntaxHighlight(); $notesList = window.$('ul.main-notes-list');
Notes.animateAppendNote(note.html, $notesList);
// Update datetime format on the recent note // Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList(); this.collapseLongCommitList();
...@@ -348,7 +350,7 @@ require('./task_list'); ...@@ -348,7 +350,7 @@ require('./task_list');
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion? // is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`);
if (!discussionContainer.length) { if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes'); discussionContainer = form.closest('.discussion').find('.notes');
} }
...@@ -370,14 +372,13 @@ require('./task_list'); ...@@ -370,14 +372,13 @@ require('./task_list');
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
} }
} }
// Init discussion on 'Discussion' page if it is merge request page // Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) { if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
$('ul.main-notes-list').append($(note.discussion_html).renderGFM()); Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list'));
} }
} else { } else {
// append new note to all matching discussions // append new note to all matching discussions
discussionContainer.append($(note.html).renderGFM()); Notes.animateAppendNote(note.html, discussionContainer);
} }
if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) { if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
...@@ -1063,6 +1064,13 @@ require('./task_list'); ...@@ -1063,6 +1064,13 @@ require('./task_list');
return $form; return $form;
}; };
Notes.animateAppendNote = function(noteHTML, $notesList) {
const $note = window.$(noteHTML);
$note.addClass('fade-in').renderGFM();
$notesList.append($note);
};
return Notes; return Notes;
})(); })();
}).call(window); }).call(window);
...@@ -65,6 +65,8 @@ export default { ...@@ -65,6 +65,8 @@ export default {
makeRequest() { makeRequest() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy');
this.service.postAction(this.endpoint) this.service.postAction(this.endpoint)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -88,9 +90,13 @@ export default { ...@@ -88,9 +90,13 @@ export default {
:aria-label="title" :aria-label="title"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
:disabled="isLoading" :disabled="isLoading">
> <i
<i :class="iconClass" aria-hidden="true"></i> :class="iconClass"
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i> aria-hidden="true" />
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
v-if="isLoading" />
</button> </button>
</template> </template>
...@@ -28,6 +28,8 @@ export default { ...@@ -28,6 +28,8 @@ export default {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -57,6 +59,7 @@ export default { ...@@ -57,6 +59,7 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
data-placement="top" data-placement="top"
aria-label="Manual job" aria-label="Manual job"
ref="tooltip"
:disabled="isLoading"> :disabled="isLoading">
${playIconSvg} ${playIconSvg}
<i <i
......
function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint');
$.ajax({
type: 'GET',
url: usageDataUrl,
dataType: 'html',
success(html) {
$('.usage-data').html(html);
},
});
}
window.gl = window.gl || {};
window.gl.UsagePing = UsagePing;
...@@ -56,6 +56,9 @@ ...@@ -56,6 +56,9 @@
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(function () {
$loading.fadeOut(); $loading.fadeOut();
})
.catch(function () {
$loading.fadeOut();
}); });
}; };
......
...@@ -145,3 +145,17 @@ a { ...@@ -145,3 +145,17 @@ a {
.dropdown-menu-nav a { .dropdown-menu-nav a {
transition: none; transition: none;
} }
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-in {
animation: fadeIn $fade-in-duration 1;
}
...@@ -40,6 +40,10 @@ ...@@ -40,6 +40,10 @@
line-height: 24px; line-height: 24px;
} }
.bold {
font-weight: 600;
}
.tab-content { .tab-content {
overflow: visible; overflow: visible;
} }
......
...@@ -564,3 +564,7 @@ ...@@ -564,3 +564,7 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
} }
.droplab-item-ignore {
pointer-events: none;
}
...@@ -331,6 +331,14 @@ header { ...@@ -331,6 +331,14 @@ header {
.dropdown-menu-nav { .dropdown-menu-nav {
min-width: 140px; min-width: 140px;
margin-top: -5px; margin-top: -5px;
.current-user {
padding: 5px 18px;
.user-name {
display: block;
}
}
} }
} }
......
...@@ -457,6 +457,11 @@ $label-inverse-bg: #333; ...@@ -457,6 +457,11 @@ $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 100px; $label-border-radius: 100px;
/*
* Animation
*/
$fade-in-duration: 200ms;
/* /*
* Lint * Lint
*/ */
......
...@@ -210,10 +210,6 @@ ...@@ -210,10 +210,6 @@
} }
} }
.bold {
font-weight: 600;
}
.light { .light {
font-weight: normal; font-weight: normal;
} }
......
...@@ -596,6 +596,10 @@ pre.light-well { ...@@ -596,6 +596,10 @@ pre.light-well {
.avatar-container { .avatar-container {
align-self: flex-start; align-self: flex-start;
> a {
width: 100%;
}
} }
.project-details { .project-details {
......
...@@ -17,6 +17,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -17,6 +17,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
end end
def usage_data
respond_to do |format|
format.html do
usage_data = Gitlab::UsageData.data
usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
end
format.json { render json: Gitlab::UsageData.to_json }
end
end
def reset_runners_token def reset_runners_token
@application_setting.reset_runners_registration_token! @application_setting.reset_runners_registration_token!
flash[:notice] = 'New runners registration token has been generated!' flash[:notice] = 'New runners registration token has been generated!'
...@@ -135,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -135,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:version_check_enabled, :version_check_enabled,
:terminal_max_session_time, :terminal_max_session_time,
:polling_interval_multiplier, :polling_interval_multiplier,
:usage_ping_enabled,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
......
class Admin::CohortsController < Admin::ApplicationController
def index
if current_application_settings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
CohortsService.new.execute
end
@cohorts = CohortsSerializer.new.represent(cohorts_results)
end
end
end
module CreatesCommit module CreatesCommit
extend ActiveSupport::Concern extend ActiveSupport::Concern
def set_start_branch_to_branch_name
branch_exists = @repository.find_branch(@branch_name)
@start_branch = @branch_name if branch_exists
end
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
set_commit_variables if can?(current_user, :push_code, @project)
@project_to_commit_into = @project
@branch_name ||= @ref
else
@project_to_commit_into = current_user.fork_of(@project)
@branch_name ||= @project_to_commit_into.repository.next_branch('patch')
end
@start_branch ||= @ref || @branch_name
commit_params = @commit_params.merge( commit_params = @commit_params.merge(
start_project: @mr_target_project, start_project: @project,
start_branch: @mr_target_branch, start_branch: @start_branch,
target_branch: @mr_source_branch branch_name: @branch_name
) )
result = service.new( result = service.new(@project_to_commit_into, current_user, commit_params).execute
@mr_source_project, current_user, commit_params).execute
if result[:status] == :success if result[:status] == :success
update_flash_notice(success_notice) update_flash_notice(success_notice)
...@@ -72,30 +84,30 @@ module CreatesCommit ...@@ -72,30 +84,30 @@ module CreatesCommit
def new_merge_request_path def new_merge_request_path
new_namespace_project_merge_request_path( new_namespace_project_merge_request_path(
@mr_source_project.namespace, @project_to_commit_into.namespace,
@mr_source_project, @project_to_commit_into,
merge_request: { merge_request: {
source_project_id: @mr_source_project.id, source_project_id: @project_to_commit_into.id,
target_project_id: @mr_target_project.id, target_project_id: @project.id,
source_branch: @mr_source_branch, source_branch: @branch_name,
target_branch: @mr_target_branch target_branch: @start_branch
} }
) )
end end
def existing_merge_request_path def existing_merge_request_path
namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request) namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
end end
def merge_request_exists? def merge_request_exists?
return @merge_request if defined?(@merge_request) return @merge_request if defined?(@merge_request)
@merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project) find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch)
end end
def different_project? def different_project?
@mr_source_project != @mr_target_project @project_to_commit_into != @project
end end
def create_merge_request? def create_merge_request?
...@@ -103,22 +115,6 @@ module CreatesCommit ...@@ -103,22 +115,6 @@ module CreatesCommit
# as the target branch in the same project, # as the target branch in the same project,
# we don't want to create a merge request. # we don't want to create a merge request.
params[:create_merge_request].present? && params[:create_merge_request].present? &&
(different_project? || @mr_target_branch != @mr_source_branch) (different_project? || @start_branch != @branch_name)
end
def set_commit_variables
if can?(current_user, :push_code, @project)
@mr_source_project = @project
@target_branch ||= @ref
else
@mr_source_project = current_user.fork_of(@project)
@target_branch ||= @mr_source_project.repository.next_branch('patch')
end
# Merge request to this project
@mr_target_project = @project
@mr_target_branch ||= @ref || @target_branch
@mr_source_branch = @target_branch
end end
end end
module MembershipActions module MembershipActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
def create
status = Members::CreateService.new(membershipable, current_user, params).execute
redirect_url = members_page_url
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
else
redirect_to redirect_url, alert: 'No users specified.'
end
end
def destroy
Members::DestroyService.new(membershipable, current_user, params).
execute(:all)
respond_to do |format|
format.html do
message = "User was successfully removed from #{source_type}."
redirect_to members_page_url, notice: message
end
format.js { head :ok }
end
end
def request_access def request_access
membershipable.request_access(current_user) membershipable.request_access(current_user)
...@@ -11,20 +37,20 @@ module MembershipActions ...@@ -11,20 +37,20 @@ module MembershipActions
def approve_access_request def approve_access_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
redirect_to polymorphic_url([membershipable, :members]) redirect_to members_page_url
end end
def leave def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all) execute(:all)
source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
else else
"You left the \"#{membershipable.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
...@@ -35,4 +61,16 @@ module MembershipActions ...@@ -35,4 +61,16 @@ module MembershipActions
def membershipable def membershipable
raise NotImplementedError raise NotImplementedError
end end
def members_page_url
if membershipable.is_a?(Project)
project_settings_members_path(membershipable)
else
polymorphic_url([membershipable, :members])
end
end
def source_type
@source_type ||= membershipable.class.to_s.humanize(capitalize: false)
end
end end
...@@ -21,21 +21,6 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -21,21 +21,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new @group_member = @group.group_members.new
end end
def create
if params[:user_ids].blank?
return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
end
@group.add_users(
params[:user_ids].split(','),
params[:access_level],
current_user: current_user,
expires_at: params[:expires_at]
)
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
def update def update
@group_member = @group.group_members.find(params[:id]) @group_member = @group.group_members.find(params[:id])
...@@ -44,15 +29,6 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -44,15 +29,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member.update_attributes(member_params) @group_member.update_attributes(member_params)
end end
def destroy
Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
format.js { head :ok }
end
end
def resend_invite def resend_invite
redirect_path = group_group_members_path(@group) redirect_path = group_group_members_path(@group)
......
...@@ -89,9 +89,4 @@ class Projects::ApplicationController < ApplicationController ...@@ -89,9 +89,4 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user) return render_404 unless @project.feature_available?(:builds, current_user)
end end
def update_ref
branch_exists = @repository.find_branch(@target_branch)
@ref = @target_branch if branch_exists
end
end end
...@@ -25,10 +25,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -25,10 +25,10 @@ class Projects::BlobController < Projects::ApplicationController
end end
def create def create
update_ref set_start_branch_to_branch_name
create_commit(Files::CreateService, success_notice: "The file has been successfully created.", create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }, success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new, failure_view: :new,
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end end
...@@ -69,10 +69,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -69,10 +69,10 @@ class Projects::BlobController < Projects::ApplicationController
end end
def destroy def destroy
create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.", create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) }, success_path: -> { namespace_project_tree_path(@project.namespace, @project, @branch_name) },
failure_view: :show, failure_view: :show,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end end
def diff def diff
...@@ -127,16 +127,16 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -127,16 +127,16 @@ class Projects::BlobController < Projects::ApplicationController
def after_edit_path def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
if from_merge_request && @target_branch == @ref if from_merge_request && @branch_name == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}" "##{hexdigest(@path)}"
else else
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @path))
end end
end end
def editor_variables def editor_variables
@target_branch = params[:target_branch] @branch_name = params[:branch_name]
@file_path = @file_path =
if action_name.to_s == 'create' if action_name.to_s == 'create'
......
...@@ -56,9 +56,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -56,9 +56,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank? return render_404 if @start_branch.blank?
@target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch @branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch
@mr_target_branch = @start_branch
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: -> { successful_change_path }, failure_path: failed_change_path) success_path: -> { successful_change_path }, failure_path: failed_change_path)
...@@ -69,9 +67,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -69,9 +67,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank? return render_404 if @start_branch.blank?
@target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
@mr_target_branch = @start_branch
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: -> { successful_change_path }, failure_path: failed_change_path) success_path: -> { successful_change_path }, failure_path: failed_change_path)
...@@ -84,7 +80,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -84,7 +80,7 @@ class Projects::CommitController < Projects::ApplicationController
end end
def successful_change_path def successful_change_path
referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @branch_name)
end end
def failed_change_path def failed_change_path
......
...@@ -5,6 +5,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -5,6 +5,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs def info_refs
if upload_pack? && upload_pack_allowed? if upload_pack? && upload_pack_allowed?
log_user_activity
render_ok render_ok
elsif receive_pack? && receive_pack_allowed? elsif receive_pack? && receive_pack_allowed?
render_ok render_ok
...@@ -106,4 +108,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -106,4 +108,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def access_klass def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess @access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end end
def log_user_activity
Users::ActivityService.new(user, 'pull').execute
end
end end
...@@ -10,18 +10,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -10,18 +10,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort) redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end end
def create
status = Members::CreateService.new(@project, current_user, params).execute
redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
else
redirect_to redirect_url, alert: 'No users or groups specified.'
end
end
def update def update
@project_member = @project.project_members.find(params[:id]) @project_member = @project.project_members.find(params[:id])
...@@ -30,18 +18,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -30,18 +18,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member.update_attributes(member_params) @project_member.update_attributes(member_params)
end end
def destroy
Members::DestroyService.new(@project, current_user, params).
execute(:all)
respond_to do |format|
format.html do
redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end
format.js { head :ok }
end
end
def resend_invite def resend_invite
redirect_path = namespace_project_settings_members_path(@project.namespace, @project) redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
......
...@@ -34,16 +34,16 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -34,16 +34,16 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir def create_dir
return render_404 unless @commit_params.values.all? return render_404 unless @commit_params.values.all?
update_ref set_start_branch_to_branch_name
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)), success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref)) failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
end end
private private
def assign_dir_vars def assign_dir_vars
@target_branch = params[:target_branch] @branch_name = params[:branch_name]
@dir_name = File.join(@path, params[:dir_name]) @dir_name = File.join(@path, params[:dir_name])
@commit_params = { @commit_params = {
......
...@@ -35,6 +35,7 @@ class SessionsController < Devise::SessionsController ...@@ -35,6 +35,7 @@ class SessionsController < Devise::SessionsController
# hide the signed-in notification # hide the signed-in notification
flash[:notice] = nil flash[:notice] = nil
log_audit_event(current_user, with: authentication_method) log_audit_event(current_user, with: authentication_method)
log_user_activity(current_user)
end end
end end
...@@ -123,6 +124,10 @@ class SessionsController < Devise::SessionsController ...@@ -123,6 +124,10 @@ class SessionsController < Devise::SessionsController
for_authentication.security_event for_authentication.security_event
end end
def log_user_activity(user)
Users::ActivityService.new(user, 'login').execute
end
def load_recaptcha def load_recaptcha
Gitlab::Recaptcha.load_configurations! Gitlab::Recaptcha.load_configurations!
end end
......
...@@ -165,11 +165,8 @@ module IssuablesHelper ...@@ -165,11 +165,8 @@ module IssuablesHelper
html.html_safe html.html_safe
end end
def cached_assigned_issuables_count(assignee, issuable_type, state) def assigned_issuables_count(issuable_type)
cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-')) current_user.public_send("assigned_open_#{issuable_type}_count")
Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
assigned_issuables_count(assignee, issuable_type, state)
end
end end
def issuable_filter_params def issuable_filter_params
...@@ -192,10 +189,6 @@ module IssuablesHelper ...@@ -192,10 +189,6 @@ module IssuablesHelper
private private
def assigned_issuables_count(assignee, issuable_type, state)
assignee.public_send("assigned_#{issuable_type}").public_send(state).count
end
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true' cookies[:collapsed_gutter] == 'true'
end end
......
...@@ -272,14 +272,14 @@ module ProjectsHelper ...@@ -272,14 +272,14 @@ module ProjectsHelper
end end
end end
def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil) def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
namespace_project_new_blob_path( namespace_project_new_blob_path(
project.namespace, project.namespace,
project, project,
project.default_branch || 'master', project.default_branch || 'master',
file_name: file_name, file_name: file_name,
commit_message: commit_message || "Add #{file_name.downcase}", commit_message: commit_message || "Add #{file_name.downcase}",
target_branch: target_branch, branch_name: branch_name,
context: context context: context
) )
end end
......
...@@ -62,6 +62,14 @@ module SortingHelper ...@@ -62,6 +62,14 @@ module SortingHelper
} }
end end
def branches_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
end
def sort_title_priority def sort_title_priority
'Priority' 'Priority'
end end
......
...@@ -35,7 +35,7 @@ module TreeHelper ...@@ -35,7 +35,7 @@ module TreeHelper
end end
def on_top_of_branch?(project = @project, ref = @ref) def on_top_of_branch?(project = @project, ref = @ref)
project.repository.branch_names.include?(ref) project.repository.branch_exists?(ref)
end end
def can_edit_tree?(project = nil, ref = nil) def can_edit_tree?(project = nil, ref = nil)
......
...@@ -238,7 +238,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -238,7 +238,8 @@ class ApplicationSetting < ActiveRecord::Base
terminal_max_session_time: 0, terminal_max_session_time: 0,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, user_default_external: false,
polling_interval_multiplier: 1 polling_interval_multiplier: 1,
usage_ping_enabled: true
} }
end end
......
...@@ -23,7 +23,7 @@ module Issuable ...@@ -23,7 +23,7 @@ module Issuable
included do included do
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description cache_markdown_field :description, issuable_state_filter_enabled: true
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
......
...@@ -7,6 +7,8 @@ class Identity < ActiveRecord::Base ...@@ -7,6 +7,8 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider }
scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) }
def ldap? def ldap?
provider.starts_with?('ldap') provider.starts_with?('ldap')
end end
......
...@@ -16,7 +16,7 @@ class Note < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class Note < ActiveRecord::Base
ignore_column :original_discussion_id ignore_column :original_discussion_id
cache_markdown_field :note, pipeline: :note cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
# Attribute containing rendered and redacted Markdown as generated by # Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer. # Banzai::ObjectRenderer.
......
...@@ -181,7 +181,7 @@ class Project < ActiveRecord::Base ...@@ -181,7 +181,7 @@ class Project < ActiveRecord::Base
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 :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, 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
delegate :empty_repo?, to: :repository delegate :empty_repo?, to: :repository
......
...@@ -99,9 +99,6 @@ class User < ActiveRecord::Base ...@@ -99,9 +99,6 @@ class User < ActiveRecord::Base
has_many :award_emoji, dependent: :destroy has_many :award_emoji, dependent: :destroy
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
# Issues that a user owns are expected to be moved to the "ghost" user before # Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this # the user is destroyed. If the user owns any issues during deletion, this
# should be treated as an exceptional condition. # should be treated as an exceptional condition.
...@@ -891,20 +888,20 @@ class User < ActiveRecord::Base ...@@ -891,20 +888,20 @@ class User < ActiveRecord::Base
@global_notification_setting @global_notification_setting
end end
def assigned_open_merge_request_count(force: false) def assigned_open_merge_requests_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
assigned_merge_requests.opened.count MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end end
end end
def assigned_open_issues_count(force: false) def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
assigned_issues.opened.count IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end end
end end
def update_cache_counts def update_cache_counts
assigned_open_merge_request_count(force: true) assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true) assigned_open_issues_count(force: true)
end end
......
class CohortActivityMonthEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
expose :total do |cohort_activity_month|
number_with_delimiter(cohort_activity_month[:total])
end
expose :percentage do |cohort_activity_month|
number_to_percentage(cohort_activity_month[:percentage], precision: 0)
end
end
class CohortEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
expose :registration_month do |cohort|
cohort[:registration_month].strftime('%b %Y')
end
expose :total do |cohort|
number_with_delimiter(cohort[:total])
end
expose :inactive do |cohort|
number_with_delimiter(cohort[:inactive])
end
expose :activity_months, using: CohortActivityMonthEntity
end
class CohortsEntity < Grape::Entity
expose :months_included
expose :cohorts, using: CohortEntity
end
class CohortsSerializer < AnalyticsGenericSerializer
entity CohortsEntity
end
class CohortsService
MONTHS_INCLUDED = 12
def execute
{
months_included: MONTHS_INCLUDED,
cohorts: cohorts
}
end
# Get an array of hashes that looks like:
#
# [
# {
# registration_month: Date.new(2017, 3),
# activity_months: [3, 2, 1],
# total: 3
# inactive: 0
# },
# etc.
#
# The `months` array is always from oldest to newest, so it's always
# non-strictly decreasing from left to right.
def cohorts
months = Array.new(MONTHS_INCLUDED) { |i| i.months.ago.beginning_of_month.to_date }
Array.new(MONTHS_INCLUDED) do
registration_month = months.last
activity_months = running_totals(months, registration_month)
# Even if no users registered in this month, we always want to have a
# value to fill in the table.
inactive = counts_by_month[[registration_month, nil]].to_i
months.pop
{
registration_month: registration_month,
activity_months: activity_months,
total: activity_months.first[:total],
inactive: inactive
}
end
end
private
# Calculate a running sum of active users, so users active in later months
# count as active in this month, too. Start with the most recent month first,
# for calculating the running totals, and then reverse for displaying in the
# table.
#
# Each month has a total, and a percentage of the overall total, as keys.
def running_totals(all_months, registration_month)
month_totals =
all_months
.map { |activity_month| counts_by_month[[registration_month, activity_month]] }
.reduce([]) { |result, total| result << result.last.to_i + total.to_i }
.reverse
overall_total = month_totals.first
month_totals.map do |total|
{ total: total, percentage: total.zero? ? 0 : 100 * total / overall_total }
end
end
# Get a hash that looks like:
#
# {
# [created_at_month, last_activity_on_month] => count,
# [created_at_month, last_activity_on_month_2] => count_2,
# # etc.
# }
#
# created_at_month can never be nil, but last_activity_on_month can (when a
# user has never logged in, just been created). This covers the last
# MONTHS_INCLUDED months.
def counts_by_month
@counts_by_month ||=
begin
created_at_month = column_to_date('created_at')
last_activity_on_month = column_to_date('last_activity_on')
User
.where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month)
.group(created_at_month, last_activity_on_month)
.reorder("#{created_at_month} ASC", "#{last_activity_on_month} ASC")
.count
end
end
def column_to_date(column)
if Gitlab::Database.postgresql?
"CAST(DATE_TRUNC('month', #{column}) AS date)"
else
"STR_TO_DATE(DATE_FORMAT(#{column}, '%Y-%m-01'), '%Y-%m-%d')"
end
end
end
module Commits module Commits
class ChangeService < ::BaseService class ChangeService < Commits::CreateService
ValidationError = Class.new(StandardError) def initialize(*args)
ChangeError = Class.new(StandardError) super
def execute
@start_project = params[:start_project] || @project
@start_branch = params[:start_branch]
@target_branch = params[:target_branch]
@commit = params[:commit] @commit = params[:commit]
check_push_permissions
commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ChangeError => ex
error(ex.message)
end end
private private
def commit
raise NotImplementedError
end
def commit_change(action) def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action) raise NotImplementedError unless repository.respond_to?(action)
validate_target_branch if different_branch?
repository.public_send( repository.public_send(
action, action,
current_user, current_user,
@commit, @commit,
@target_branch, @branch_name,
start_project: @start_project, start_project: @start_project,
start_branch_name: @start_branch) start_branch_name: @start_branch)
success
rescue Repository::CreateTreeError rescue Repository::CreateTreeError
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg raise ChangeError, error_msg
end end
def check_push_permissions
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
end
true
end
def validate_target_branch
result = ValidateNewBranchService.new(@project, current_user)
.execute(@target_branch)
if result[:status] == :error
raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
end
end
def different_branch?
@start_branch != @target_branch || @start_project != @project
end
end end
end end
module Commits module Commits
class CherryPickService < ChangeService class CherryPickService < ChangeService
def commit def create_commit!
commit_change(:cherry_pick) commit_change(:cherry_pick)
end end
end end
......
module Commits
class CreateService < ::BaseService
ValidationError = Class.new(StandardError)
ChangeError = Class.new(StandardError)
def initialize(*args)
super
@start_project = params[:start_project] || @project
@start_branch = params[:start_branch]
@branch_name = params[:branch_name]
end
def execute
validate!
new_commit = create_commit!
success(result: new_commit)
rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, GitHooksService::PreReceiveError => ex
error(ex.message)
end
private
def create_commit!
raise NotImplementedError
end
def raise_error(message)
raise ValidationError, message
end
def different_branch?
@start_branch != @branch_name || @start_project != @project
end
def validate!
validate_permissions!
validate_on_branch!
validate_branch_existance!
validate_new_branch_name! if different_branch?
end
def validate_permissions!
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@branch_name)
unless allowed
raise_error("You are not allowed to push into this branch")
end
end
def validate_on_branch!
if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
raise_error('You can only create or edit files when you are on a branch')
end
end
def validate_branch_existance!
if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
raise_error("A branch called '#{@branch_name}' already exists. Switch to that branch in order to make changes")
end
end
def validate_new_branch_name!
result = ValidateNewBranchService.new(project, current_user).execute(@branch_name)
if result[:status] == :error
raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}")
end
end
end
end
module Commits module Commits
class RevertService < ChangeService class RevertService < ChangeService
def commit def create_commit!
commit_change(:revert) commit_change(:revert)
end end
end end
......
...@@ -8,9 +8,20 @@ class DeleteMergedBranchesService < BaseService ...@@ -8,9 +8,20 @@ class DeleteMergedBranchesService < BaseService
branches = project.repository.branch_names branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
branches.each do |branch| branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch) DeleteBranchService.new(project, current_user).execute(branch)
end end
end end
private
def merge_request_branch_names
# reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch)
target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch)
(source_names + target_names).uniq
end
end end
...@@ -72,6 +72,8 @@ class EventCreateService ...@@ -72,6 +72,8 @@ class EventCreateService
def push(project, current_user, push_data) def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data) create_event(project, current_user, Event::PUSHED, data: push_data)
Users::ActivityService.new(current_user, 'push').execute
end end
private private
......
module Files module Files
class BaseService < ::BaseService class BaseService < Commits::CreateService
ValidationError = Class.new(StandardError) def initialize(*args)
super
def execute
@start_project = params[:start_project] || @project
@start_branch = params[:start_branch]
@target_branch = params[:target_branch]
@author_email = params[:author_email]
@author_name = params[:author_name]
@commit_message = params[:commit_message] @commit_message = params[:commit_message]
@file_path = params[:file_path]
@previous_path = params[:previous_path]
@file_content = if params[:file_content_encoding] == 'base64'
Base64.decode64(params[:file_content])
else
params[:file_content]
end
@last_commit_sha = params[:last_commit_sha]
@author_email = params[:author_email]
@author_name = params[:author_name]
# Validate parameters
validate
# Create new branch if it different from start_branch
validate_target_branch if different_branch?
result = commit
if result
success(result: result)
else
error('Something went wrong. Your changes were not committed')
end
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message)
end
private
def different_branch?
@start_branch != @target_branch || @start_project != @project
end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def raise_error(message)
raise ValidationError.new(message)
end
def validate
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
unless allowed
raise_error("You are not allowed to push into this branch")
end
if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
raise ValidationError, 'You can only create or edit files when you are on a branch'
end
if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes"
end
end
def validate_target_branch @file_path = params[:file_path]
result = ValidateNewBranchService.new(project, current_user). @previous_path = params[:previous_path]
execute(@target_branch)
if result[:status] == :error @file_content = params[:file_content]
raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") @file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
end end
end end
end end
module Files module Files
class CreateDirService < Files::BaseService class CreateDirService < Files::BaseService
def commit def create_commit!
repository.create_dir( repository.create_dir(
current_user, current_user,
@file_path, @file_path,
message: @commit_message, message: @commit_message,
branch_name: @target_branch, branch_name: @branch_name,
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
start_project: @start_project, start_project: @start_project,
start_branch_name: @start_branch) start_branch_name: @start_branch)
end end
def validate
super
unless @file_path =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file path ' +
Gitlab::Regex.file_path_regex_message
)
end
end
end end
end end
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def commit def create_commit!
repository.create_file( repository.create_file(
current_user, current_user,
@file_path, @file_path,
@file_content, @file_content,
message: @commit_message, message: @commit_message,
branch_name: @target_branch, branch_name: @branch_name,
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
start_project: @start_project, start_project: @start_project,
start_branch_name: @start_branch) start_branch_name: @start_branch)
end end
def validate
super
if @file_content.nil?
raise_error("You must provide content.")
end
if @file_path =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name ' +
Gitlab::Regex.directory_traversal_regex_message
)
end
unless @file_path =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name ' +
Gitlab::Regex.file_path_regex_message
)
end
unless project.empty_repo?
@file_path.slice!(0) if @file_path.start_with?('/')
blob = repository.blob_at_branch(@start_branch, @file_path)
if blob
raise_error('Your changes could not be committed because a file with the same name already exists')
end
end
end
end end
end end
module Files module Files
class DestroyService < Files::BaseService class DeleteService < Files::BaseService
def commit def create_commit!
repository.delete_file( repository.delete_file(
current_user, current_user,
@file_path, @file_path,
message: @commit_message, message: @commit_message,
branch_name: @target_branch, branch_name: @branch_name,
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
start_project: @start_project, start_project: @start_project,
......
module Files module Files
class MultiService < Files::BaseService class MultiService < Files::BaseService
FileChangedError = Class.new(StandardError) def create_commit!
ACTIONS = %w[create update delete move].freeze
def commit
repository.multi_action( repository.multi_action(
user: current_user, user: current_user,
message: @commit_message, message: @commit_message,
branch_name: @target_branch, branch_name: @branch_name,
actions: params[:actions], actions: params[:actions],
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
...@@ -19,122 +15,17 @@ module Files ...@@ -19,122 +15,17 @@ module Files
private private
def validate def validate!
super super
params[:actions].each_with_index do |action, index| params[:actions].each do |action|
if ACTIONS.include?(action[:action].to_s) validate_action!(action)
action[:action] = action[:action].to_sym
else
raise_error("Unknown action type `#{action[:action]}`.")
end
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
if project.empty_repo? && action[:action] != :create
raise_error("No files to #{action[:action]}.")
end
validate_file_exists(action)
case action[:action]
when :create
validate_create(action)
when :update
validate_update(action)
when :delete
validate_delete(action)
when :move
validate_move(action, index)
end
end
end
def validate_file_exists(action)
return if action[:action] == :create
file_path = action[:file_path]
file_path = action[:previous_path] if action[:action] == :move
blob = repository.blob_at_branch(params[:branch], file_path)
unless blob
raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
end end
end end
def last_commit def validate_action!(action)
Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path) unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s)
end raise_error("Unknown action '#{action[:action]}'")
def regex_check(file)
if file =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.directory_traversal_regex_message
)
end
unless file =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.file_path_regex_message
)
end
end
def validate_create(action)
return if project.empty_repo?
if repository.blob_at_branch(params[:branch], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
raise_error("You must provide content.")
end
end
def validate_update(action)
if action[:content].nil?
raise_error("You must provide content.")
end
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
def validate_delete(action)
end
def validate_move(action, index)
if action[:previous_path].nil?
raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
end
blob = repository.blob_at_branch(params[:branch], action[:file_path])
if blob
raise_error("Move destination `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
blob = repository.blob_at_branch(params[:branch], action[:previous_path])
blob.load_all_data!(repository) if blob.truncated?
params[:actions][index][:content] = blob.data
end end
end end
end end
......
...@@ -2,10 +2,16 @@ module Files ...@@ -2,10 +2,16 @@ module Files
class UpdateService < Files::BaseService class UpdateService < Files::BaseService
FileChangedError = Class.new(StandardError) FileChangedError = Class.new(StandardError)
def commit def initialize(*args)
super
@last_commit_sha = params[:last_commit_sha]
end
def create_commit!
repository.update_file(current_user, @file_path, @file_content, repository.update_file(current_user, @file_path, @file_content,
message: @commit_message, message: @commit_message,
branch_name: @target_branch, branch_name: @branch_name,
previous_path: @previous_path, previous_path: @previous_path,
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
...@@ -15,21 +21,23 @@ module Files ...@@ -15,21 +21,23 @@ module Files
private private
def validate def file_has_changed?
super return false unless @last_commit_sha && last_commit
if @file_content.nil?
raise_error("You must provide content.")
end
if file_has_changed? @last_commit_sha != last_commit.sha
raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
end
end end
def last_commit def last_commit
@last_commit ||= Gitlab::Git::Commit. @last_commit ||= Gitlab::Git::Commit.
last_for_path(@start_project.repository, @start_branch, @file_path) last_for_path(@start_project.repository, @start_branch, @file_path)
end end
def validate!
super
if file_has_changed?
raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
end
end
end end
end end
...@@ -9,7 +9,11 @@ module Members ...@@ -9,7 +9,11 @@ module Members
def execute def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy Member.transaction do
unassign_issues_and_merge_requests(member)
member.destroy
end
if member.request? && member.user != user if member.request? && member.user != user
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
...@@ -17,5 +21,23 @@ module Members ...@@ -17,5 +21,23 @@ module Members
member member
end end
private
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
update_all(assignee_id: nil)
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
update_all(assignee_id: nil)
else
project = member.source
project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil)
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end
end
end end
end end
module Members module Members
class CreateService < BaseService class CreateService < BaseService
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute def execute
return false if params[:user_ids].blank? return false if params[:user_ids].blank?
project.team.add_users( @source.add_users(
params[:user_ids].split(','), params[:user_ids].split(','),
params[:access_level], params[:access_level],
expires_at: params[:expires_at], expires_at: params[:expires_at],
......
module Users
class ActivityService
def initialize(author, activity)
@author = author.respond_to?(:user) ? author.user : author
@activity = activity
end
def execute
return unless @author && @author.is_a?(User)
record_activity
end
private
def record_activity
Gitlab::UserActivities.record(@author.id)
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
end
end
end
...@@ -8,10 +8,7 @@ class ValidateNewBranchService < BaseService ...@@ -8,10 +8,7 @@ class ValidateNewBranchService < BaseService
return error('Branch name is invalid') return error('Branch name is invalid')
end end
repository = project.repository if project.repository.branch_exists?(branch_name)
existing_branch = repository.find_branch(branch_name)
if existing_branch
return error('Branch already exists') return error('Branch already exists')
end end
......
...@@ -477,7 +477,7 @@ ...@@ -477,7 +477,7 @@
diagrams in Asciidoc documents using an external PlantUML service. diagrams in Asciidoc documents using an external PlantUML service.
%fieldset %fieldset
%legend Usage statistics %legend#usage-statistics Usage statistics
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
...@@ -486,6 +486,19 @@ ...@@ -486,6 +486,19 @@
Version check enabled Version check enabled
.help-block .help-block
Let GitLab inform you when an update is available. Let GitLab inform you when an update is available.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled
Usage ping enabled
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-data")
.help-block
Every week GitLab will report license usage back to GitLab, Inc.
Disable this option if you do not want this to occur. To see the
JSON payload that will be sent, visit the
= succeed '.' do
= link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
%fieldset %fieldset
%legend Email %legend Email
......
.bs-callout.clearfix
%p
User cohorts are shown for the last #{@cohorts[:months_included]}
months. Only users with activity are counted in the cohort total; inactive
users are counted separately.
= link_to icon('question-circle'), help_page_path('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
.table-holder
%table.table
%thead
%tr
%th Registration month
%th Inactive users
%th Cohort total
- @cohorts[:months_included].times do |i|
%th Month #{i}
%tbody
- @cohorts[:cohorts].each do |cohort|
%tr
%td= cohort[:registration_month]
%td= cohort[:inactive]
%td= cohort[:total]
- cohort[:activity_months].each do |activity_month|
%td
- next if cohort[:total] == '0'
= activity_month[:percentage]
%br
= activity_month[:total]
%h2#usage-ping Usage ping
.bs-callout.clearfix
%p
User cohorts are shown because the usage ping is enabled. The data sent with
this is shown below. To disable this, visit
= succeed '.' do
= link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html, pretty: true) } }
- @no_container = true
= render "admin/dashboard/head"
%div{ class: container_class }
- if @cohorts
= render 'cohorts_table'
= render 'usage_ping'
- else
.bs-callout.bs-callout-warning.clearfix
%p
User cohorts are only shown when the
= link_to 'usage ping', help_page_path('user/admin_area/usage_statistics'), target: '_blank'
is enabled. To enable it and see user cohorts,
visit
= succeed '.' do
= link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
...@@ -27,3 +27,7 @@ ...@@ -27,3 +27,7 @@
= link_to admin_runners_path, title: 'Runners' do = link_to admin_runners_path, title: 'Runners' do
%span %span
Runners Runners
= nav_link path: 'cohorts#index' do
= link_to admin_cohorts_path, title: 'Cohorts' do
%span
Cohorts
...@@ -47,13 +47,13 @@ ...@@ -47,13 +47,13 @@
%li %li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw') = icon('hashtag fw')
- issues_count = cached_assigned_issuables_count(current_user, :issues, :opened) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li %li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
- merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li %li
...@@ -67,6 +67,11 @@ ...@@ -67,6 +67,11 @@
= icon('caret-down') = icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
%li.current-user
.user-name.bold
= current_user.name
@#{current_user.username}
%li.divider
%li %li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li %li
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
I I
%span %span
Issues Issues
.badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) .badge= number_with_delimiter(assigned_issuables_count(:issues))
= nav_link(path: 'dashboard#merge_requests') do = nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.shortcut-mappings .shortcut-mappings
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
M M
%span %span
Merge Requests Merge Requests
.badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) .badge= number_with_delimiter(assigned_issuables_count(:merge_requests))
= nav_link(controller: 'dashboard/snippets') do = nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
.shortcut-mappings .shortcut-mappings
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- if @conflict - if @conflict
.alert.alert-danger .alert.alert-danger
Someone edited the file the same time you did. Please check out Someone edited the file the same time you did. Please check out
= link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer' = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs. and make sure your changes will not unintentionally remove theirs.
.editor-title-row .editor-title-row
%h3.page-title.blob-edit-page-title %h3.page-title.blob-edit-page-title
......
...@@ -15,16 +15,14 @@ ...@@ -15,16 +15,14 @@
.dropdown.inline> .dropdown.inline>
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light %span.light
= projects_sort_options_hash[@sort] = branches_sort_options_hash[@sort]
= icon('chevron-down') = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li %li.dropdown-header
= link_to filter_branches_path(sort: sort_value_name) do Sort by
= sort_title_name - branches_sort_options_hash.each do |value, title|
= link_to filter_branches_path(sort: sort_value_recently_updated) do %li
= sort_title_recently_updated = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value)
= link_to filter_branches_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
- if can? current_user, :push_code, @project - if can? current_user, :push_code, @project
= link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
......
...@@ -13,9 +13,6 @@ ...@@ -13,9 +13,6 @@
Environment: Environment:
= link_to @environment.name, environment_path(@environment) = link_to @environment.name, environment_path(@environment)
.col-sm-6
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.prometheus-state .prometheus-state
.js-getting-started.hidden .js-getting-started.hidden
.row .row
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
= render 'projects/merge_requests/widget/open/conflicts' = render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress? - elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip' = render 'projects/merge_requests/widget/open/wip'
- elsif @merge_request.merge_when_pipeline_succeeds? && @merge_request.merge_error.present?
= render 'projects/merge_requests/widget/open/error'
- elsif @merge_request.merge_when_pipeline_succeeds? - elsif @merge_request.merge_when_pipeline_succeeds?
= render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds' = render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user) - elsif !@merge_request.can_be_merged_by?(current_user)
......
%h4
= icon('exclamation-triangle')
This merge request failed to be merged automatically
%p
= @merge_request.merge_error
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%div{ class: container_class } %div{ class: container_class }
%h3.page-title %h3.page-title
Edit Milestone #{@milestone.to_reference} Edit Milestone
%hr %hr
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
.header-text-content .header-text-content
%span.identifier %span.identifier
%strong %strong
Milestone #{@milestone.to_reference} Milestone
- if @milestone.due_date || @milestone.start_date - if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone) = milestone_date_range(@milestone)
.milestone-buttons .milestone-buttons
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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