Commit a91ba228 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-support-bitbucket-server-import

parents 57d1b60f 35ce06ad
...@@ -439,13 +439,12 @@ setup-test-env: ...@@ -439,13 +439,12 @@ setup-test-env:
- vendor/gitaly-ruby - vendor/gitaly-ruby
danger-review: danger-review:
image: registry.gitlab.com/gitlab-org/gitaly/dangercontainer:latest image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test stage: test
allow_failure: true allow_failure: true
before_script:
- source scripts/utils.sh
- retry gem install danger --no-ri --no-rdoc
cache: {} cache: {}
dependencies: []
before_script: []
only: only:
variables: variables:
- $DANGER_GITLAB_API_TOKEN - $DANGER_GITLAB_API_TOKEN
......
...@@ -10,9 +10,9 @@ AllCops: ...@@ -10,9 +10,9 @@ AllCops:
Exclude: Exclude:
- 'vendor/**/*' - 'vendor/**/*'
- 'node_modules/**/*' - 'node_modules/**/*'
- 'db/**/*'
- 'db/fixtures/**/*' - 'db/fixtures/**/*'
- 'ee/db/**/*' - 'db/schema.rb'
- 'ee/db/geo/schema.rb'
- 'tmp/**/*' - 'tmp/**/*'
- 'bin/**/*' - 'bin/**/*'
- 'generator_templates/**/*' - 'generator_templates/**/*'
...@@ -34,6 +34,8 @@ Style/MutableConstant: ...@@ -34,6 +34,8 @@ Style/MutableConstant:
Naming/FileName: Naming/FileName:
ExpectMatchingDefinition: true ExpectMatchingDefinition: true
Exclude: Exclude:
- 'db/**/*'
- 'ee/db/**/*'
- 'spec/**/*' - 'spec/**/*'
- 'features/**/*' - 'features/**/*'
- 'ee/spec/**/*' - 'ee/spec/**/*'
......
...@@ -2,6 +2,42 @@ ...@@ -2,6 +2,42 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.1.3 (2018-07-27)
### Fixed (8 changes, 1 of them is from the community)
- Rework some projects table indexes around repository_storage field. !20377
- Fix navigation to First and Next discussion on MR Changes tab. !20434
- Fix showing outdated discussions on Changes tab. !20445
- Fix autosave and ESC confirmation issues for MR discussions. !20569
- Fix rendering of the context lines in MR diffs page. !20642
- Don't overflow project/group dropdown results. !20704 (gfyoung)
- Fixed IDE not opening JSON files. !20798
- Disable Gitaly timeouts when creating or restoring backups. !20810
### Performance (1 change)
- Reduces the client side memory footprint on merge requests. !20744
## 11.1.2 (2018-07-26)
### Security (4 changes)
- Adding CSRF protection to Hooks test action.
- Don't expose project names in GitHub counters.
- Don't expose project names in various counters.
- Fixed XSS in branch name in Web IDE.
### Fixed (1 change)
- Escapes milestone and label's names on flash notice when promoting them.
### Performance (1 change)
- Fix slow Markdown rendering. !20820
## 11.1.1 (2018-07-23) ## 11.1.1 (2018-07-23)
### Fixed (2 changes) ### Fixed (2 changes)
...@@ -253,6 +289,20 @@ entry. ...@@ -253,6 +289,20 @@ entry.
- Use monospaced font for MR diff commit link ref on GFM. - Use monospaced font for MR diff commit link ref on GFM.
## 11.0.5 (2018-07-26)
### Security (4 changes)
- Don't expose project names in various counters.
- Don't expose project names in GitHub counters.
- Adding CSRF protection to Hooks test action.
- Fixed XSS in branch name in Web IDE.
### Fixed (1 change)
- Escapes milestone and label's names on flash notice when promoting them.
## 11.0.4 (2018-07-17) ## 11.0.4 (2018-07-17)
### Security (1 change) ### Security (1 change)
......
...@@ -400,6 +400,7 @@ gem 'email_reply_trimmer', '~> 0.1' ...@@ -400,6 +400,7 @@ gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text' gem 'html2text'
gem 'ruby-prof', '~> 0.17.0' gem 'ruby-prof', '~> 0.17.0'
gem 'rbtrace', '~> 0.4', require: false
# OAuth # OAuth
gem 'oauth2', '~> 1.4' gem 'oauth2', '~> 1.4'
......
...@@ -375,7 +375,8 @@ GEM ...@@ -375,7 +375,8 @@ GEM
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
haml (4.0.7) haml (5.0.4)
temple (>= 0.8.0)
tilt tilt
haml_lint (0.26.0) haml_lint (0.26.0)
haml (>= 4.0, < 5.1) haml (>= 4.0, < 5.1)
...@@ -700,6 +701,10 @@ GEM ...@@ -700,6 +701,10 @@ GEM
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbtrace (0.4.10)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
rdoc (6.0.4) rdoc (6.0.4)
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (3.0.0)
...@@ -911,6 +916,7 @@ GEM ...@@ -911,6 +916,7 @@ GEM
parslet (~> 1.5.0) parslet (~> 1.5.0)
toml-rb (1.0.0) toml-rb (1.0.0)
citrus (~> 3.0, > 3.0) citrus (~> 3.0, > 3.0)
trollop (2.1.3)
truncato (0.7.10) truncato (0.7.10)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.8.0, >= 1.7.0) nokogiri (~> 1.8.0, >= 1.7.0)
...@@ -1133,6 +1139,7 @@ DEPENDENCIES ...@@ -1133,6 +1139,7 @@ DEPENDENCIES
rainbow (~> 2.2) rainbow (~> 2.2)
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.0) rdoc (~> 6.0)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 3.0) recaptcha (~> 3.0)
......
...@@ -364,7 +364,7 @@ GEM ...@@ -364,7 +364,7 @@ GEM
grape-entity (0.7.1) grape-entity (0.7.1)
activesupport (>= 4.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-path-helpers (1.0.5) grape-path-helpers (1.0.6)
activesupport (>= 4, < 5.1) activesupport (>= 4, < 5.1)
grape (~> 1.0) grape (~> 1.0)
rake (~> 12) rake (~> 12)
...@@ -378,7 +378,8 @@ GEM ...@@ -378,7 +378,8 @@ GEM
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
haml (4.0.7) haml (5.0.4)
temple (>= 0.8.0)
tilt tilt
haml_lint (0.26.0) haml_lint (0.26.0)
haml (>= 4.0, < 5.1) haml (>= 4.0, < 5.1)
...@@ -400,7 +401,7 @@ GEM ...@@ -400,7 +401,7 @@ GEM
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
html-pipeline (2.8.3) html-pipeline (2.8.4)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
html2text (0.2.0) html2text (0.2.0)
...@@ -518,7 +519,7 @@ GEM ...@@ -518,7 +519,7 @@ GEM
net-ssh (5.0.1) net-ssh (5.0.1)
netrc (0.11.0) netrc (0.11.0)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.8.3) nokogiri (1.8.4)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0) nokogumbo (1.5.0)
nokogiri nokogiri
...@@ -709,6 +710,10 @@ GEM ...@@ -709,6 +710,10 @@ GEM
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbtrace (0.4.10)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
rdoc (6.0.4) rdoc (6.0.4)
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (3.0.0)
...@@ -817,7 +822,7 @@ GEM ...@@ -817,7 +822,7 @@ GEM
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.2) rugged (0.27.2)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.5) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4) nokogumbo (~> 1.4)
...@@ -911,13 +916,14 @@ GEM ...@@ -911,13 +916,14 @@ GEM
rack (>= 1, < 3) rack (>= 1, < 3)
thor (0.19.4) thor (0.19.4)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.6) tilt (2.0.8)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
toml (0.1.2) toml (0.1.2)
parslet (~> 1.5.0) parslet (~> 1.5.0)
toml-rb (1.0.0) toml-rb (1.0.0)
citrus (~> 3.0, > 3.0) citrus (~> 3.0, > 3.0)
trollop (2.1.3)
truncato (0.7.10) truncato (0.7.10)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.8.0, >= 1.7.0) nokogiri (~> 1.8.0, >= 1.7.0)
...@@ -1144,6 +1150,7 @@ DEPENDENCIES ...@@ -1144,6 +1150,7 @@ DEPENDENCIES
rainbow (~> 2.2) rainbow (~> 2.2)
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.0) rdoc (~> 6.0)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 3.0) recaptcha (~> 3.0)
......
...@@ -93,7 +93,7 @@ export default { ...@@ -93,7 +93,7 @@ export default {
<icon <icon
:size="16" :size="16"
class="prepend-left-8 append-right-8" class="prepend-left-8 append-right-8"
name="doc_image" name="doc-image"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
......
/* eslint-disable quote-props, comma-dangle */
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
...@@ -47,7 +45,7 @@ export default () => { ...@@ -47,7 +45,7 @@ export default () => {
gl.IssueBoardsApp = new Vue({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
'board': gl.issueBoards.Board, board: gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar, 'board-sidebar': gl.issueBoards.BoardSidebar,
BoardAddIssuesModal, BoardAddIssuesModal,
}, },
...@@ -65,11 +63,11 @@ export default () => { ...@@ -65,11 +63,11 @@ export default () => {
defaultAvatar: $boardApp.dataset.defaultAvatar, defaultAvatar: $boardApp.dataset.defaultAvatar,
}, },
computed: { computed: {
detailIssueVisible () { detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length; return Object.keys(this.detailIssue.issue).length;
}, },
}, },
created () { created() {
gl.boardService = new BoardService({ gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint, boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint, listsEndpoint: this.listsEndpoint,
...@@ -89,15 +87,16 @@ export default () => { ...@@ -89,15 +87,16 @@ export default () => {
eventHub.$off('clearDetailIssue', this.clearDetailIssue); eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
}, },
mounted () { mounted() {
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup(); this.filterManager.setup();
Store.disabled = this.disabled; Store.disabled = this.disabled;
gl.boardService.all() gl.boardService
.all()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
data.forEach((board) => { data.forEach(board => {
const list = Store.addList(board, this.defaultAvatar); const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') { if (list.type === 'closed') {
...@@ -126,7 +125,7 @@ export default () => { ...@@ -126,7 +125,7 @@ export default () => {
newIssue.setFetchingState('subscriptions', true); newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint) BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
newIssue.setFetchingState('subscriptions', false); newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({ newIssue.updateData({
subscribed: data.subscribed, subscribed: data.subscribed,
...@@ -159,7 +158,7 @@ export default () => { ...@@ -159,7 +158,7 @@ export default () => {
Flash(__('An error occurred when toggling the notification subscription')); Flash(__('An error occurred when toggling the notification subscription'));
}); });
} }
} },
}, },
}); });
...@@ -168,77 +167,81 @@ export default () => { ...@@ -168,77 +167,81 @@ export default () => {
data: { data: {
filters: Store.state.filters, filters: Store.state.filters,
}, },
mounted () { mounted() {
gl.issueBoards.newListDropdownInit(); gl.issueBoards.newListDropdownInit();
}, },
}); });
gl.IssueBoardsModalAddBtn = new Vue({ const issueBoardsModal = document.getElementById('js-add-issues-btn');
el: document.getElementById('js-add-issues-btn'),
mixins: [modalMixin], if (issueBoardsModal) {
data() { gl.IssueBoardsModalAddBtn = new Vue({
return { el: issueBoardsModal,
modal: ModalStore.store, mixins: [modalMixin],
store: Store.state, data() {
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), return {
}; modal: ModalStore.store,
}, store: Store.state,
computed: { canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
disabled() { };
if (!this.store) {
return true;
}
return !this.store.lists.filter(list => !list.preset).length;
}, },
tooltipTitle() { computed: {
if (this.disabled) { disabled() {
return 'Please add a list to your board first'; if (!this.store) {
} return true;
}
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
if (this.disabled) {
return 'Please add a list to your board first';
}
return ''; return '';
},
}, },
}, watch: {
watch: { disabled() {
disabled() { this.updateTooltip();
},
},
mounted() {
this.updateTooltip(); this.updateTooltip();
}, },
}, methods: {
mounted() { updateTooltip() {
this.updateTooltip(); const $tooltip = $(this.$refs.addIssuesButton);
},
methods: { this.$nextTick(() => {
updateTooltip() { if (this.disabled) {
const $tooltip = $(this.$refs.addIssuesButton); $tooltip.tooltip();
} else {
this.$nextTick(() => { $tooltip.tooltip('dispose');
if (this.disabled) { }
$tooltip.tooltip(); });
} else { },
$tooltip.tooltip('dispose'); openModal() {
if (!this.disabled) {
this.toggleModal(true);
} }
}); },
},
openModal() {
if (!this.disabled) {
this.toggleModal(true);
}
}, },
}, template: `
template: ` <div class="board-extra-actions">
<div class="board-extra-actions"> <button
<button class="btn btn-create prepend-left-10"
class="btn btn-create prepend-left-10" type="button"
type="button" data-placement="bottom"
data-placement="bottom" ref="addIssuesButton"
ref="addIssuesButton" :class="{ 'disabled': disabled }"
:class="{ 'disabled': disabled }" :title="tooltipTitle"
:title="tooltipTitle" :aria-disabled="disabled"
:aria-disabled="disabled" v-if="canAdminList"
v-if="canAdminList" @click="openModal">
@click="openModal"> Add issues
Add issues </button>
</button> </div>
</div> `,
`, });
}); }
}; };
import Vue from 'vue';
import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar';
Vue.component('gl-progress-bar', progressBar);
...@@ -3,4 +3,5 @@ import './polyfills'; ...@@ -3,4 +3,5 @@ import './polyfills';
import './jquery'; import './jquery';
import './bootstrap'; import './bootstrap';
import './vue'; import './vue';
import './gitlab_ui';
import '../lib/utils/axios_utils'; import '../lib/utils/axios_utils';
...@@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; ...@@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility'; import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { getCommitIconMap } from '../utils';
export default { export default {
components: { components: {
...@@ -34,16 +35,14 @@ export default { ...@@ -34,16 +35,14 @@ export default {
}, },
computed: { computed: {
changedIcon() { changedIcon() {
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile && !this.forceModifiedIcon
? `file-addition${suffix}` if (this.forceModifiedIcon) return `file-modified${suffix}`;
: `file-modified${suffix}`;
}, return `${getCommitIconMap(this.file).icon}${suffix}`;
stagedIcon() {
return `${this.changedIcon}-solid`;
}, },
changedIconClass() { changedIconClass() {
return `multi-${this.changedIcon} float-left`; return `ide-${this.changedIcon} float-left`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
...@@ -66,6 +65,9 @@ export default { ...@@ -66,6 +65,9 @@ export default {
return undefined; return undefined;
}, },
showIcon() {
return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted;
},
}, },
}; };
</script> </script>
...@@ -79,7 +81,7 @@ export default { ...@@ -79,7 +81,7 @@ export default {
class="ide-file-changed-icon" class="ide-file-changed-icon"
> >
<icon <icon
v-if="file.changed || file.tempFile || file.staged" v-if="showIcon"
:name="changedIcon" :name="changedIcon"
:size="12" :size="12"
:css-classes="changedIconClass" :css-classes="changedIconClass"
......
<script> <script>
import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants'; import * as consts from '../../stores/modules/commit/constants';
...@@ -14,7 +15,7 @@ export default { ...@@ -14,7 +15,7 @@ export default {
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${this.currentBranchId}</strong>` }, { branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` },
false, false,
); );
}, },
......
...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants'; import { viewerTypes } from '../../constants';
import { getCommitIconMap } from '../../utils';
export default { export default {
components: { components: {
...@@ -42,11 +43,12 @@ export default { ...@@ -42,11 +43,12 @@ export default {
}, },
computed: { computed: {
iconName() { iconName() {
const prefix = this.stagedList ? '-solid' : ''; const suffix = this.stagedList ? '-solid' : '';
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
return `${getCommitIconMap(this.file).icon}${suffix}`;
}, },
iconClass() { iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; return `${getCommitIconMap(this.file).class} append-right-8`;
}, },
fullKey() { fullKey() {
return `${this.keyPrefix}-${this.file.key}`; return `${this.keyPrefix}-${this.file.key}`;
...@@ -67,6 +69,8 @@ export default { ...@@ -67,6 +69,8 @@ export default {
'stageChange', 'stageChange',
]), ]),
openFileInEditor() { openFileInEditor() {
if (this.file.type === 'tree') return null;
return this.openPendingTab({ return this.openPendingTab({
file: this.file, file: this.file,
keyPrefix: this.keyPrefix, keyPrefix: this.keyPrefix,
......
...@@ -56,7 +56,7 @@ export default { ...@@ -56,7 +56,7 @@ export default {
> >
<icon <icon
:size="12" :size="12"
name="more" name="ellipsis_h"
/> />
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
......
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
EditorModeDropdown, EditorModeDropdown,
}, },
computed: { computed: {
...mapGetters(['currentMergeRequest']), ...mapGetters(['currentMergeRequest', 'activeFile']),
...mapState(['viewer', 'currentMergeRequestId']), ...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() { showLatestChangesText() {
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
...@@ -23,12 +23,20 @@ export default { ...@@ -23,12 +23,20 @@ export default {
}, },
}, },
mounted() { mounted() {
if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
} else if (this.activeFile && this.activeFile.deleted) {
this.resetOpenFiles();
}
this.$nextTick(() => { this.$nextTick(() => {
this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
}); });
}, },
methods: { methods: {
...mapActions(['updateViewer']), ...mapActions(['updateViewer', 'resetOpenFiles']),
}, },
}; };
</script> </script>
...@@ -36,7 +44,6 @@ export default { ...@@ -36,7 +44,6 @@ export default {
<template> <template>
<ide-tree-list <ide-tree-list
:viewer-type="viewer" :viewer-type="viewer"
:disable-action-dropdown="true"
header-class="ide-review-header" header-class="ide-review-header"
> >
<template <template
......
...@@ -17,14 +17,18 @@ export default { ...@@ -17,14 +17,18 @@ export default {
...mapGetters(['currentProject', 'currentTree', 'activeFile']), ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
}, },
mounted() { mounted() {
if (this.activeFile && this.activeFile.pending) { if (!this.activeFile) return;
if (this.activeFile.pending && !this.activeFile.deleted) {
this.$router.push(`/project${this.activeFile.url}`, () => { this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor'); this.updateViewer('editor');
}); });
} else if (this.activeFile.deleted) {
this.resetOpenFiles();
} }
}, },
methods: { methods: {
...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']),
}, },
}; };
</script> </script>
......
...@@ -22,11 +22,6 @@ export default { ...@@ -22,11 +22,6 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapState(['currentBranchId']), ...mapState(['currentBranchId']),
...@@ -69,7 +64,6 @@ export default { ...@@ -69,7 +64,6 @@ export default {
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:disable-action-dropdown="disableActionDropdown"
/> />
</template> </template>
</div> </div>
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
ItemButton, ItemButton,
}, },
props: { props: {
branch: { type: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['createTempEntry', 'openNewEntryModal']), ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
createNewItem(type) { createNewItem(type) {
this.openNewEntryModal({ type, path: this.path }); this.openNewEntryModal({ type, path: this.path });
this.dropdownOpen = false; this.dropdownOpen = false;
...@@ -82,28 +82,40 @@ export default { ...@@ -82,28 +82,40 @@ export default {
ref="dropdownMenu" ref="dropdownMenu"
class="dropdown-menu dropdown-menu-right" class="dropdown-menu dropdown-menu-right"
> >
<template v-if="type === 'tree'">
<li>
<item-button
:label="__('New file')"
class="d-flex"
icon="doc-new"
icon-classes="mr-2"
@click="createNewItem('blob')"
/>
</li>
<li>
<upload
:path="path"
@create="createTempEntry"
/>
</li>
<li>
<item-button
:label="__('New directory')"
class="d-flex"
icon="folder-new"
icon-classes="mr-2"
@click="createNewItem('tree')"
/>
</li>
<li class="divider"></li>
</template>
<li> <li>
<item-button <item-button
:label="__('New file')" :label="__('Delete')"
class="d-flex" class="d-flex"
icon="doc-new" icon="remove"
icon-classes="mr-2" icon-classes="mr-2"
@click="createNewItem('blob')" @click="deleteEntry(path)"
/>
</li>
<li>
<upload
:path="path"
@create="createTempEntry"
/>
</li>
<li>
<item-button
:label="__('New directory')"
class="d-flex"
icon="folder-new"
icon-classes="mr-2"
@click="createNewItem('tree')"
/> />
</li> </li>
</ul> </ul>
......
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
}, },
}, },
mounted() { mounted() {
if (this.lastOpenedFile) { if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') {
this.openPendingTab({ this.openPendingTab({
file: this.lastOpenedFile, file: this.lastOpenedFile,
keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
......
...@@ -87,7 +87,9 @@ export default { ...@@ -87,7 +87,9 @@ export default {
this.editor.updateDimensions(); this.editor.updateDimensions();
}, },
viewer() { viewer() {
this.createEditorInstance(); if (!this.file.pending) {
this.createEditorInstance();
}
}, },
panelResizing() { panelResizing() {
if (!this.panelResizing) { if (!this.panelResizing) {
...@@ -109,6 +111,7 @@ export default { ...@@ -109,6 +111,7 @@ export default {
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'getFileData',
'getRawFileData', 'getRawFileData',
'changeFileContent', 'changeFileContent',
'setFileLanguage', 'setFileLanguage',
...@@ -123,10 +126,16 @@ export default { ...@@ -123,10 +126,16 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.getRawFileData({ this.getFileData({
path: this.file.path, path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', makeFileActive: false,
}) })
.then(() =>
this.getRawFileData({
path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}),
)
.then(() => { .then(() => {
this.createEditorInstance(); this.createEditorInstance();
}) })
...@@ -246,6 +255,8 @@ export default { ...@@ -246,6 +255,8 @@ export default {
ref="editor" ref="editor"
:class="{ :class="{
'is-readonly': isCommitModeActive, 'is-readonly': isCommitModeActive,
'is-deleted': file.deleted,
'is-added': file.tempFile
}" }"
class="multi-file-editor-holder" class="multi-file-editor-holder"
> >
......
...@@ -34,11 +34,6 @@ export default { ...@@ -34,11 +34,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -212,8 +207,7 @@ export default { ...@@ -212,8 +207,7 @@ export default {
/> />
</span> </span>
<new-dropdown <new-dropdown
v-if="isTree && !disableActionDropdown" :type="file.type"
:project-id="file.projectId"
:branch="file.branchId" :branch="file.branchId"
:path="file.path" :path="file.path"
:mouse-over="mouseOver" :mouse-over="mouseOver"
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
return this.fileHasChanged ? !this.tabMouseOver : false; return this.fileHasChanged ? !this.tabMouseOver : false;
}, },
fileHasChanged() { fileHasChanged() {
return this.tab.changed || this.tab.tempFile || this.tab.staged; return this.tab.changed || this.tab.tempFile || this.tab.staged || this.tab.deleted;
}, },
}, },
...@@ -71,7 +71,8 @@ export default { ...@@ -71,7 +71,8 @@ export default {
<template> <template>
<li <li
:class="{ :class="{
active: tab.active active: tab.active,
disabled: tab.pending
}" }"
@click="clickFile(tab)" @click="clickFile(tab)"
@mouseover="mouseOverTab" @mouseover="mouseOverTab"
...@@ -105,7 +106,6 @@ export default { ...@@ -105,7 +106,6 @@ export default {
<changed-file-icon <changed-file-icon
v-else v-else
:file="tab" :file="tab"
:force-modified-icon="true"
/> />
</button> </button>
</li> </li>
......
...@@ -38,3 +38,18 @@ export const stageKeys = { ...@@ -38,3 +38,18 @@ export const stageKeys = {
unstaged: 'unstaged', unstaged: 'unstaged',
staged: 'staged', staged: 'staged',
}; };
export const commitItemIconMap = {
addition: {
icon: 'file-addition',
class: 'ide-file-addition',
},
modified: {
icon: 'file-modified',
class: 'ide-file-modified',
},
deleted: {
icon: 'file-deletion',
class: 'ide-file-deletion',
},
};
...@@ -7,7 +7,7 @@ export default class Model { ...@@ -7,7 +7,7 @@ export default class Model {
this.disposable = new Disposable(); this.disposable = new Disposable();
this.file = file; this.file = file;
this.head = head; this.head = head;
this.content = file.content !== '' ? file.content : file.raw; this.content = file.content !== '' || file.deleted ? file.content : file.raw;
this.disposable.add( this.disposable.add(
(this.originalModel = monacoEditor.createModel( (this.originalModel = monacoEditor.createModel(
......
...@@ -18,7 +18,7 @@ export default { ...@@ -18,7 +18,7 @@ export default {
return axios return axios
.get(file.rawPath, { .get(file.rawPath, {
params: { format: 'json' }, transformResponse: [f => f],
}) })
.then(({ data }) => data); .then(({ data }) => data);
}, },
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
return axios return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), { .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' }, transformResponse: [f => f],
}) })
.then(({ data }) => data); .then(({ data }) => data);
}, },
......
...@@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { ...@@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
$('#ide-new-entry').modal('show'); $('#ide-new-entry').modal('show');
}; };
export const deleteEntry = ({ commit, dispatch, state }, path) => {
dispatch('burstUnusedSeal');
dispatch('closeFile', state.entries[path]);
commit(types.DELETE_ENTRY, path);
};
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path]; const file = state.entries[path];
if (file.raw || file.tempFile) return Promise.resolve();
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
return service return service
.getFileData( .getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
...@@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
commit(types.SET_FILE_DATA, { data, file }); commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, path); if (makeFileActive) commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path); if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
}) })
...@@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
service service
.getRawFileData(file) .getRawFileData(file)
.then(raw => { .then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) { if (file.mrChange && file.mrChange.new_file === false) {
service service
.getBaseRawFileData(file, baseSha) .getBaseRawFileData(file, baseSha)
......
...@@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => { ...@@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => {
export const handleTreeEntryAction = ({ commit, dispatch }, row) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') { if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path); dispatch('toggleTreeOpen', row.path);
} else if (row.type === 'blob' && (row.opened || row.changed)) { } else if (row.type === 'blob') {
if (row.changed && !row.opened) { if (!row.opened) {
commit(types.TOGGLE_FILE_OPEN, row.path); commit(types.TOGGLE_FILE_OPEN, row.path);
} }
dispatch('setFileActive', row.path); dispatch('setFileActive', row.path);
} else {
dispatch('getFileData', { path: row.path });
} }
dispatch('showTreeEntry', row.path); dispatch('showTreeEntry', row.path);
......
...@@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateActivityBarView', activityBarViews.edit, { root: true }); dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
dispatch('updateViewer', 'editor', { root: true }); dispatch('updateViewer', 'editor', { root: true });
router.push( if (rootGetters.activeFile) {
`/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ router.push(
rootGetters.activeFile.path `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
}`, rootGetters.activeFile.path
); }`,
);
}
} }
}) })
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
......
import { sprintf, n__ } from '../../../../locale'; import { sprintf, n__, __ } from '../../../../locale';
import * as consts from './constants'; import * as consts from './constants';
const BRANCH_SUFFIX_COUNT = 5; const BRANCH_SUFFIX_COUNT = 5;
const createTranslatedTextForFiles = (files, text) => {
if (!files.length) return null;
return sprintf(n__('%{text} %{files}', '%{text} %{files} files', files.length), {
files: files.reduce((acc, val) => acc.concat(val.path), []).join(', '),
text,
});
};
export const discardDraftButtonDisabled = state => export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading; state.commitMessage === '' || state.submitCommitLoading;
...@@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => { ...@@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => {
export const preBuiltCommitMessage = (state, _, rootState) => { export const preBuiltCommitMessage = (state, _, rootState) => {
if (state.commitMessage) return state.commitMessage; if (state.commitMessage) return state.commitMessage;
const files = (rootState.stagedFiles.length const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles;
? rootState.stagedFiles const modifiedFiles = files.filter(f => !f.deleted);
: rootState.changedFiles const deletedFiles = files.filter(f => f.deleted);
).reduce((acc, val) => acc.concat(val.path), []);
return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), { return [
files: files.join(', '), createTranslatedTextForFiles(modifiedFiles, __('Update')),
}); createTranslatedTextForFiles(deletedFiles, __('Deleted')),
]
.filter(t => t)
.join('\n');
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
...@@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; ...@@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request'; import mergeRequestMutation from './mutations/merge_request';
...@@ -171,6 +172,16 @@ export default { ...@@ -171,6 +172,16 @@ export default {
newEntryModal: { type, path }, newEntryModal: { type, path },
}); });
}, },
[types.DELETE_ENTRY](state, path) {
const entry = state.entries[path];
const parent = entry.parentPath
? state.entries[entry.parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
entry.deleted = true;
state.changedFiles = state.changedFiles.concat(entry);
parent.tree = parent.tree.filter(f => f.path !== entry.path);
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { sortTree } from '../utils';
import { diffModes } from '../../constants'; import { diffModes } from '../../constants';
export default { export default {
...@@ -51,9 +52,17 @@ export default { ...@@ -51,9 +52,17 @@ export default {
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
const openPendingFile = state.openFiles.find(
f => f.path === file.path && f.pending && !f.tempFile,
);
Object.assign(state.entries[file.path], { Object.assign(state.entries[file.path], {
raw, raw,
}); });
if (openPendingFile) {
openPendingFile.raw = raw;
}
}, },
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], { Object.assign(state.entries[file.path], {
...@@ -109,11 +118,22 @@ export default { ...@@ -109,11 +118,22 @@ export default {
}, },
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path];
const { deleted } = entry;
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: stagedFile ? stagedFile.content : state.entries[path].raw, content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false, changed: false,
deleted: false,
}); });
if (deleted) {
const parent = entry.parentPath
? state.entries[entry.parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
parent.tree = sortTree(parent.tree.concat(entry));
}
}, },
[types.ADD_FILE_TO_CHANGED](state, path) { [types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, { Object.assign(state, {
......
...@@ -46,6 +46,7 @@ export const dataStructure = () => ({ ...@@ -46,6 +46,7 @@ export const dataStructure = () => ({
parentPath: null, parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
mrChange: null, mrChange: null,
deleted: false,
}); });
export const decorateData = entity => { export const decorateData = entity => {
...@@ -105,15 +106,37 @@ export const setPageTitle = title => { ...@@ -105,15 +106,37 @@ export const setPageTitle = title => {
document.title = title; document.title = title;
}; };
export const commitActionForFile = file => {
if (file.deleted) {
return 'delete';
} else if (file.tempFile) {
return 'create';
}
return 'update';
};
export const getCommitFiles = (stagedFiles, deleteTree = false) =>
stagedFiles.reduce((acc, file) => {
if ((file.deleted || deleteTree) && file.type === 'tree') {
return acc.concat(getCommitFiles(file.tree, true));
}
return acc.concat({
...file,
deleted: deleteTree || file.deleted,
});
}, []);
export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({ export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({
branch, branch,
commit_message: state.commitMessage || getters.preBuiltCommitMessage, commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: rootState.stagedFiles.map(f => ({ actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: f.tempFile ? 'create' : 'update', action: commitActionForFile(f),
file_path: f.path, file_path: f.path,
content: f.content, content: f.content,
encoding: f.base64 ? 'base64' : 'text', encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch ? undefined : f.lastCommitSha, last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha,
})), })),
start_branch: newBranch ? rootState.currentBranchId : undefined, start_branch: newBranch ? rootState.currentBranchId : undefined,
}); });
......
import { commitItemIconMap } from './constants';
// eslint-disable-next-line import/prefer-default-export
export const getCommitIconMap = file => {
if (file.deleted) {
return commitItemIconMap.deleted;
} else if (file.tempFile) {
return commitItemIconMap.addition;
}
return commitItemIconMap.modified;
};
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import JobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
...@@ -46,7 +47,7 @@ export default { ...@@ -46,7 +47,7 @@ export default {
computed: { computed: {
tooltipText() { tooltipText() {
return `${this.job.name} - ${this.job.status.label}`; return _.escape(`${this.job.name} - ${this.job.status.label}`);
}, },
}, },
......
<script> <script>
import _ from 'underscore';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
...@@ -26,7 +27,8 @@ export default { ...@@ -26,7 +27,8 @@ export default {
methods: { methods: {
capitalizeStageName(name) { capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1); const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
}, },
isFirstColumn(index) { isFirstColumn(index) {
......
<script> <script>
import _ from 'underscore';
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
...@@ -61,7 +62,7 @@ export default { ...@@ -61,7 +62,7 @@ export default {
const textBuilder = []; const textBuilder = [];
if (this.job.name) { if (this.job.name) {
textBuilder.push(this.job.name); textBuilder.push(_.escape(this.job.name));
} }
if (this.job.name && this.status.tooltip) { if (this.job.name && this.status.tooltip) {
...@@ -69,7 +70,7 @@ export default { ...@@ -69,7 +70,7 @@ export default {
} }
if (this.status.tooltip) { if (this.status.tooltip) {
textBuilder.push(`${this.job.status.tooltip}`); textBuilder.push(this.job.status.tooltip);
} }
return textBuilder.join(' '); return textBuilder.join(' ');
......
<script> <script>
import _ from 'underscore';
import JobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import DropdownJobComponent from './dropdown_job_component.vue'; import DropdownJobComponent from './dropdown_job_component.vue';
...@@ -37,7 +38,7 @@ export default { ...@@ -37,7 +38,7 @@ export default {
}, },
jobId(job) { jobId(job) {
return `ci-badge-${job.name}`; return `ci-badge-${_.escape(job.name)}`;
}, },
buildConnnectorClass(index) { buildConnnectorClass(index) {
......
...@@ -42,11 +42,14 @@ export default { ...@@ -42,11 +42,14 @@ export default {
return this.timeEstimate - this.timeSpent; return this.timeEstimate - this.timeSpent;
}, },
timeRemainingPercent() { timeRemainingPercent() {
return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; return Math.floor((this.timeSpent / this.timeEstimate) * 100);
}, },
timeRemainingStatusClass() { timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
}, },
progressBarVariant() {
return this.timeRemainingPercent > 100 ? 'danger' : 'primary';
},
}, },
}; };
</script> </script>
...@@ -62,16 +65,10 @@ export default { ...@@ -62,16 +65,10 @@ export default {
data-placement="top" data-placement="top"
role="timeRemainingDisplay" role="timeRemainingDisplay"
> >
<div <gl-progress-bar
:aria-valuenow="timeRemainingPercent" :value="timeRemainingPercent"
class="meter-container" :variant="progressBarVariant"
> />
<div
:style="{ width: timeRemainingPercent }"
class="meter-fill"
>
</div>
</div>
<div class="compare-display-container"> <div class="compare-display-container">
<div class="compare-display float-left"> <div class="compare-display float-left">
<span class="compare-label"> <span class="compare-label">
......
This diff is collapsed.
export const GRADIENT_COLORS = ['#000', '#a7a7a7'];
export const GRADIENT_OPACITY = ['0', '0.4'];
export const INVERSE_GRADIENT_COLORS = ['#a7a7a7', '#000'];
export const INVERSE_GRADIENT_OPACITY = ['0.4', '0'];
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
}, },
computed: { computed: {
className() { className() {
return `drag${this.side}`; return `drag-${this.side}`;
}, },
cursorStyle() { cursorStyle() {
if (this.enabled) { if (this.enabled) {
...@@ -44,8 +44,15 @@ ...@@ -44,8 +44,15 @@
methods: { methods: {
resetSize(e) { resetSize(e) {
e.preventDefault(); e.preventDefault();
this.$emit('resize-start', this.size);
this.size = this.startSize; this.size = this.startSize;
this.$emit('update:size', this.size); this.$emit('update:size', this.size);
// End resizing on next tick so that listeners can react to DOM changes
this.$nextTick(() => {
this.$emit('resize-end', this.size);
});
}, },
startDrag(e) { startDrag(e) {
if (this.enabled) { if (this.enabled) {
...@@ -84,7 +91,7 @@ ...@@ -84,7 +91,7 @@
<div <div
:class="className" :class="className"
:style="cursorStyle" :style="cursorStyle"
class="dragHandle" class="drag-handle"
@mousedown="startDrag" @mousedown="startDrag"
@dblclick="resetSize" @dblclick="resetSize"
></div> ></div>
......
export const STATUS_FAILED = 'failed';
export const STATUS_SUCCESS = 'success';
export const STATUS_NEUTRAL = 'neutral';
export const components = {};
export const componentNames = {};
<script>
import Icon from '~/vue_shared/components/icon.vue';
import {
STATUS_FAILED,
STATUS_NEUTRAL,
STATUS_SUCCESS,
} from '~/vue_shared/components/reports/constants';
export default {
name: 'IssueStatusIcon',
components: {
Icon,
},
props: {
// failed || success
status: {
type: String,
required: true,
},
},
computed: {
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
} else if (this.isStatusSuccess) {
return 'status_success_borderless';
}
return 'status_created_borderless';
},
isStatusFailed() {
return this.status === STATUS_FAILED;
},
isStatusSuccess() {
return this.status === STATUS_SUCCESS;
},
isStatusNeutral() {
return this.status === STATUS_NEUTRAL;
},
},
};
</script>
<template>
<div
:class="{
failed: isStatusFailed,
success: isStatusSuccess,
neutral: isStatusNeutral,
}"
class="report-block-list-icon"
>
<icon
:name="iconName"
:size="32"
/>
</div>
</template>
<script> <script>
import IssuesBlock from './report_issues.vue'; import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue';
import {
STATUS_SUCCESS,
STATUS_FAILED,
STATUS_NEUTRAL,
} from '~/vue_shared/components/reports/constants';
/** /**
* Renders block of issues * Renders block of issues
...@@ -9,6 +14,9 @@ export default { ...@@ -9,6 +14,9 @@ export default {
components: { components: {
IssuesBlock, IssuesBlock,
}, },
success: STATUS_SUCCESS,
failed: STATUS_FAILED,
neutral: STATUS_NEUTRAL,
props: { props: {
unresolvedIssues: { unresolvedIssues: {
type: Array, type: Array,
...@@ -25,29 +33,10 @@ export default { ...@@ -25,29 +33,10 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
allIssues: { component: {
type: Array,
required: false,
default: () => [],
},
type: {
type: String, type: String,
required: true, required: false,
}, default: '',
},
data() {
return {
isFullReportVisible: false,
};
},
computed: {
unresolvedIssuesStatus() {
return this.type === 'license' ? 'neutral' : 'failed';
},
},
methods: {
openFullReport() {
this.isFullReportVisible = true;
}, },
}, },
}; };
...@@ -57,43 +46,26 @@ export default { ...@@ -57,43 +46,26 @@ export default {
<issues-block <issues-block
v-if="unresolvedIssues.length" v-if="unresolvedIssues.length"
:type="type" :component="component"
:status="unresolvedIssuesStatus"
:issues="unresolvedIssues" :issues="unresolvedIssues"
:status="$options.failed"
class="js-mr-code-new-issues" class="js-mr-code-new-issues"
/> />
<issues-block
v-if="isFullReportVisible"
:type="type"
:issues="allIssues"
class="js-mr-code-all-issues"
status="failed"
/>
<issues-block <issues-block
v-if="neutralIssues.length" v-if="neutralIssues.length"
:type="type" :component="component"
:issues="neutralIssues" :issues="neutralIssues"
:status="$options.neutral"
class="js-mr-code-non-issues" class="js-mr-code-non-issues"
status="neutral"
/> />
<issues-block <issues-block
v-if="resolvedIssues.length" v-if="resolvedIssues.length"
:type="type" :component="component"
:issues="resolvedIssues" :issues="resolvedIssues"
:status="$options.success"
class="js-mr-code-resolved-issues" class="js-mr-code-resolved-issues"
status="success"
/> />
<button
v-if="allIssues.length && !isFullReportVisible"
type="button"
class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
@click="openFullReport"
>
{{ s__("ciReport|Show complete code vulnerabilities report") }}
</button>
</div> </div>
</template> </template>
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue';
import { components, componentNames } from '~/vue_shared/components/reports/issue_body';
export default { export default {
name: 'ReportIssues', name: 'ReportIssues',
components: { components: {
Icon, IssueStatusIcon,
...components,
}, },
props: { props: {
issues: { issues: {
type: Array, type: Array,
required: true, required: true,
}, },
type: { component: {
type: String, type: String,
required: true, required: false,
default: '',
validator: value => value === '' || Object.values(componentNames).includes(value),
}, },
// failed || success // failed || success
status: { status: {
...@@ -21,26 +25,6 @@ export default { ...@@ -21,26 +25,6 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
} else if (this.isStatusSuccess) {
return 'status_success_borderless';
}
return 'status_created_borderless';
},
isStatusFailed() {
return this.status === 'failed';
},
isStatusSuccess() {
return this.status === 'success';
},
isStatusNeutral() {
return this.status === 'neutral';
},
},
}; };
</script> </script>
<template> <template>
...@@ -52,20 +36,17 @@ export default { ...@@ -52,20 +36,17 @@ export default {
:key="index" :key="index"
class="report-block-list-issue" class="report-block-list-issue"
> >
<div <issue-status-icon
:class="{ :status="issue.status || status"
failed: isStatusFailed, class="append-right-5"
success: isStatusSuccess, />
neutral: isStatusNeutral,
}"
class="report-block-list-icon append-right-5"
>
<icon
:name="iconName"
:size="32"
/>
</div>
<component
v-if="component"
:is="component"
:issue="issue"
:status="issue.status || status"
/>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -21,7 +21,7 @@ export default { ...@@ -21,7 +21,7 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
type: { component: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
...@@ -59,11 +59,6 @@ export default { ...@@ -59,11 +59,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
allIssues: {
type: Array,
required: false,
default: () => [],
},
infoText: { infoText: {
type: [String, Boolean], type: [String, Boolean],
required: false, required: false,
...@@ -142,18 +137,10 @@ export default { ...@@ -142,18 +137,10 @@ export default {
</script> </script>
<template> <template>
<section class="media-section"> <section class="media-section">
<div <div class="media">
class="media" <status-icon :status="statusIconName" />
> <div class="media-body space-children d-flex flex-align-self-center">
<status-icon <span class="js-code-text code-text">
:status="statusIconName"
/>
<div
class="media-body space-children d-flex"
>
<span
class="js-code-text code-text"
>
{{ headerText }} {{ headerText }}
<popover <popover
...@@ -163,10 +150,12 @@ export default { ...@@ -163,10 +150,12 @@ export default {
/> />
</span> </span>
<slot name="actionButtons"></slot>
<button <button
v-if="isCollapsible" v-if="isCollapsible"
type="button" type="button"
class="js-collapse-btn btn bt-default float-right btn-sm" class="js-collapse-btn btn float-right btn-sm"
@click="toggleCollapsed" @click="toggleCollapsed"
> >
{{ collapseText }} {{ collapseText }}
...@@ -183,8 +172,8 @@ export default { ...@@ -183,8 +172,8 @@ export default {
<issues-list <issues-list
:unresolved-issues="unresolvedIssues" :unresolved-issues="unresolvedIssues"
:resolved-issues="resolvedIssues" :resolved-issues="resolvedIssues"
:all-issues="allIssues" :neutral-issues="neutralIssues"
:type="type" :component="component"
/> />
</slot> </slot>
</div> </div>
......
<script>
export default {
props: {
colors: {
type: Array,
required: true,
},
opacity: {
type: Array,
required: true,
},
identifierName: {
type: String,
required: true,
},
},
};
</script>
<template>
<svg
height="0"
width="0">
<defs>
<linearGradient
:id="identifierName">
<stop
:stop-color="colors[0]"
:stop-opacity="opacity[0]"
offset="0%" />
<stop
:stop-color="colors[1]"
:stop-opacity="opacity[1]"
offset="100%" />
</linearGradient>
</defs>
</svg>
</template>
...@@ -370,11 +370,14 @@ img.emoji { ...@@ -370,11 +370,14 @@ img.emoji {
margin-right: 10px; margin-right: 10px;
} }
.alert, .alert {
.progress {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
} }
.progress {
height: 4px;
}
.project-item-select-holder { .project-item-select-holder {
display: inline-block; display: inline-block;
position: relative; position: relative;
......
...@@ -567,9 +567,6 @@ ...@@ -567,9 +567,6 @@
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
.mx-auto { .mx-auto {
margin: 8px 0;
text-align: center;
.tanuki-logo, .tanuki-logo,
img { img {
height: 36px; height: 36px;
......
...@@ -179,6 +179,10 @@ ...@@ -179,6 +179,10 @@
font-weight: inherit; font-weight: inherit;
} }
a > code {
color: $gl-link-color;
}
dd { dd {
margin-left: $gl-padding; margin-left: $gl-padding;
} }
......
...@@ -835,3 +835,5 @@ $font-family-monospace: $monospace-font; ...@@ -835,3 +835,5 @@ $font-family-monospace: $monospace-font;
$input-line-height: 20px; $input-line-height: 20px;
$btn-line-height: 20px; $btn-line-height: 20px;
$table-accent-bg: $gray-light; $table-accent-bg: $gray-light;
$card-border-color: $border-color;
$card-cap-bg: $gray-light;
@import 'framework/variables';
@import 'framework/mixins';
.project-refs-form, .project-refs-form,
.project-refs-target-form { .project-refs-target-form {
display: inline-block; display: inline-block;
...@@ -74,6 +77,7 @@ ...@@ -74,6 +77,7 @@
.ide-file-icon-holder { .ide-file-icon-holder {
display: flex; display: flex;
align-items: center; align-items: center;
color: $theme-gray-700;
} }
.ide-file-changed-icon { .ide-file-changed-icon {
...@@ -161,12 +165,23 @@ ...@@ -161,12 +165,23 @@
background-color: $white-light; background-color: $white-light;
border-bottom-color: $white-light; border-bottom-color: $white-light;
} }
&:not(.disabled) {
.multi-file-tab {
cursor: pointer;
}
}
&.disabled {
.multi-file-tab-close {
cursor: default;
}
}
} }
} }
.multi-file-tab { .multi-file-tab {
@include str-truncated(141px); @include str-truncated(141px);
cursor: pointer;
svg { svg {
vertical-align: middle; vertical-align: middle;
...@@ -241,6 +256,38 @@ ...@@ -241,6 +256,38 @@
} }
} }
.is-deleted {
.editor.modified {
.margin-view-overlays,
.lines-content,
.decorationsOverviewRuler {
// !important to override monaco inline styles
display: none !important;
}
}
.diffOverviewRuler.modified {
// !important to override monaco inline styles
display: none !important;
}
}
.is-added {
.editor.original {
.margin-view-overlays,
.lines-content,
.decorationsOverviewRuler {
// !important to override monaco inline styles
display: none !important;
}
}
.diffOverviewRuler.original {
// !important to override monaco inline styles
display: none !important;
}
}
.monaco-diff-editor.vs { .monaco-diff-editor.vs {
.editor.modified { .editor.modified {
box-shadow: none; box-shadow: none;
...@@ -557,16 +604,21 @@ ...@@ -557,16 +604,21 @@
} }
} }
.multi-file-addition, .ide-file-addition,
.multi-file-addition-solid { .ide-file-addition-solid {
color: $green-500; color: $green-500;
} }
.multi-file-modified, .ide-file-modified,
.multi-file-modified-solid { .ide-file-modified-solid {
color: $orange-500; color: $orange-500;
} }
.ide-file-deletion,
.ide-file-deletion-solid {
color: $red-500;
}
.multi-file-commit-list-collapsed { .multi-file-commit-list-collapsed {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -781,18 +833,21 @@ ...@@ -781,18 +833,21 @@
} }
} }
.dragHandle { .drag-handle {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 1px; width: 4px;
background-color: $white-dark;
&:hover {
background-color: $white-normal;
}
&.dragright { &.drag-right {
right: 0; right: 0;
} }
&.dragleft { &.drag-left {
left: 0; left: 0;
} }
} }
...@@ -1014,6 +1069,10 @@ ...@@ -1014,6 +1069,10 @@
.ide-new-btn { .ide-new-btn {
margin-left: auto; margin-left: auto;
} }
button {
color: $gl-text-color;
}
} }
.ide-sidebar-branch-title { .ide-sidebar-branch-title {
......
...@@ -270,6 +270,7 @@ ...@@ -270,6 +270,7 @@
.block { .block {
width: 100%; width: 100%;
word-break: break-word;
&:last-child { &:last-child {
border-bottom: 1px solid $border-gray-normal; border-bottom: 1px solid $border-gray-normal;
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
svg { svg {
vertical-align: middle; vertical-align: middle;
top: -1px;
} }
} }
......
...@@ -31,3 +31,61 @@ ...@@ -31,3 +31,61 @@
color: $gl-text-red; color: $gl-text-red;
} }
} }
.svg-graph-container {
width: 100%;
.axis-tick {
opacity: 0.4;
}
.tick-text {
fill: $gl-text-color-secondary;
}
.x-axis-text {
fill: $theme-gray-900;
}
.bar-rect {
fill: rgba($blue-500, 0.1);
stroke: $blue-500;
}
.bar-rect:hover {
fill: rgba($blue-700, 0.3);
}
.y-axis-label {
line {
stroke: $stat-graph-axis-fill;
}
text {
font-weight: bold;
font-size: 12px;
fill: $theme-gray-800;
}
}
}
.svg-graph-container-with-grab {
cursor: grab;
cursor: -webkit-grab;
}
.svg-graph-container-grabbed {
cursor: grabbing;
cursor: -webkit-grabbing;
}
@keyframes flickerAnimation {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
.animate-flicker {
animation: flickerAnimation 1.5s infinite;
fill: $theme-gray-500;
}
...@@ -834,17 +834,7 @@ ...@@ -834,17 +834,7 @@
} }
.compare-meter { .compare-meter {
&.within_estimate {
.meter-fill {
background: $gl-primary;
}
}
&.over_estimate { &.over_estimate {
.meter-fill {
background: $red-500;
}
.time-remaining, .time-remaining,
.compare-value.spent { .compare-value.spent {
color: $red-500; color: $red-500;
...@@ -852,18 +842,6 @@ ...@@ -852,18 +842,6 @@
} }
} }
.meter-container {
background: $border-gray-light;
border-radius: 3px;
.meter-fill {
max-width: 100%;
height: 5px;
border-radius: 3px;
background: $gl-primary;
}
}
.compare-display-container { .compare-display-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
......
...@@ -237,7 +237,7 @@ ...@@ -237,7 +237,7 @@
} }
.login-page-broadcast { .login-page-broadcast {
margin-top: 50px; margin-top: 40px;
} }
.navless-container { .navless-container {
......
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
height: 16px; height: 16px;
background-size: cover; background-size: cover;
&.gl-snippet-icon-doc_code { background-position: 0 0; } &.gl-snippet-icon-doc-code { background-position: 0 0; }
&.gl-snippet-icon-doc_text { background-position: 0 -16px; } &.gl-snippet-icon-doc-text { background-position: 0 -16px; }
&.gl-snippet-icon-download { background-position: 0 -32px; } &.gl-snippet-icon-download { background-position: 0 -32px; }
} }
......
...@@ -4,13 +4,17 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -4,13 +4,17 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include RendersCommits include RendersCommits
before_action :whitelist_query_limiting before_action :whitelist_query_limiting, except: :commits_root
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :set_commits before_action :set_commits, except: :commits_root
before_action :set_request_format, only: :show before_action :set_request_format, only: :show
def commits_root
redirect_to project_commits_path(@project, @project.default_branch)
end
def show def show
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
......
...@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin begin
return render_404 unless promote_service.execute(@label) return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe flash[:notice] = flash_notice_for(@label, @project.group)
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to(project_labels_path(@project), status: :see_other) redirect_to(project_labels_path(@project), status: :see_other)
...@@ -135,6 +135,15 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -135,6 +135,15 @@ class Projects::LabelsController < Projects::ApplicationController
end end
end end
def flash_notice_for(label, group)
notice = ''.html_safe
notice << label.title
notice << ' promoted to '
notice << view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group))
notice << '.'
notice
end
protected protected
def label_params def label_params
......
...@@ -76,8 +76,8 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -76,8 +76,8 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
flash[:notice] = flash_notice_for(promoted_milestone, project.group)
flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to project_milestones_path(project) redirect_to project_milestones_path(project)
...@@ -90,6 +90,15 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -90,6 +90,15 @@ class Projects::MilestonesController < Projects::ApplicationController
redirect_to milestone, alert: error.message redirect_to milestone, alert: error.message
end end
def flash_notice_for(milestone, group)
notice = ''.html_safe
notice << milestone.title
notice << ' promoted to '
notice << view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid))
notice << '.'
notice
end
def destroy def destroy
return access_denied! unless can?(current_user, :admin_milestone, @project) return access_denied! unless can?(current_user, :admin_milestone, @project)
......
...@@ -7,7 +7,7 @@ class Admin::ProjectsFinder ...@@ -7,7 +7,7 @@ class Admin::ProjectsFinder
end end
def execute def execute
items = Project.without_deleted.with_statistics items = Project.without_deleted.with_statistics.with_route
items = by_namespace_id(items) items = by_namespace_id(items)
items = by_visibilty_level(items) items = by_visibilty_level(items)
items = by_with_push(items) items = by_with_push(items)
...@@ -16,7 +16,7 @@ class Admin::ProjectsFinder ...@@ -16,7 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items) items = by_archived(items)
items = by_personal(items) items = by_personal(items)
items = by_name(items) items = by_name(items)
items = items.includes(namespace: [:owner]) items = items.includes(namespace: [:owner, :route])
sort(items).page(params[:page]) sort(items).page(params[:page])
end end
......
...@@ -7,5 +7,5 @@ class GitlabSchema < GraphQL::Schema ...@@ -7,5 +7,5 @@ class GitlabSchema < GraphQL::Schema
query(Types::QueryType) query(Types::QueryType)
default_max_page_size 100 default_max_page_size 100
# mutation(Types::MutationType) mutation(Types::MutationType)
end end
# frozen_string_literal: true
module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
field :errors, [GraphQL::STRING_TYPE],
null: false,
description: "Reasons why the mutation failed."
def current_user
context[:current_user]
end
end
end
module Mutations
module ResolvesProject
extend ActiveSupport::Concern
def resolve_project(full_path:)
resolver.resolve(full_path: full_path)
end
def resolver
Resolvers::ProjectResolver.new(object: nil, context: context)
end
end
end
module Mutations
module MergeRequests
class Base < BaseMutation
include Gitlab::Graphql::Authorize::AuthorizeResource
include Mutations::ResolvesProject
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: "The project the merge request to mutate is in"
argument :iid, GraphQL::ID_TYPE,
required: true,
description: "The iid of the merge request to mutate"
field :merge_request,
Types::MergeRequestType,
null: true,
description: "The merge request after mutation"
authorize :update_merge_request
private
def find_object(project_path:, iid:)
project = resolve_project(full_path: project_path)
resolver = Resolvers::MergeRequestResolver.new(object: project, context: context)
resolver.resolve(iid: iid)
end
end
end
end
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetWip < Base
graphql_name 'MergeRequestSetWip'
argument :wip,
GraphQL::BOOLEAN_TYPE,
required: true,
description: <<~DESC
Whether or not to set the merge request as a WIP.
DESC
def resolve(project_path:, iid:, wip: nil)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
::MergeRequests::UpdateService.new(project, current_user, wip_event: wip_event(merge_request, wip))
.execute(merge_request)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
private
def wip_event(merge_request, wip)
wip ? 'wip' : 'unwip'
end
end
end
end
# frozen_string_literal: true
module Types module Types
class MutationType < BaseObject class MutationType < BaseObject
include Gitlab::Graphql::MountMutation
graphql_name "Mutation" graphql_name "Mutation"
# TODO: Add Mutations as fields mount_mutation Mutations::MergeRequests::SetWip
end end
end end
...@@ -4,4 +4,23 @@ module EnvironmentsHelper ...@@ -4,4 +4,23 @@ module EnvironmentsHelper
endpoint: project_environments_path(@project, format: :json) endpoint: project_environments_path(@project, format: :json)
} }
end end
def metrics_data(project, environment)
{
"settings-path" => edit_project_service_path(project, 'prometheus'),
"clusters-path" => project_clusters_path(project),
"current-environment-name": environment.name,
"documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
"empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
"deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json),
"environments-endpoint": project_environments_path(project, format: :json),
"project-path" => project_path(project),
"tags-path" => project_tags_path(project),
"has-metrics" => "#{environment.has_metrics?}"
}
end
end end
...@@ -10,7 +10,7 @@ module HooksHelper ...@@ -10,7 +10,7 @@ module HooksHelper
trigger_human_name = trigger.to_s.tr('_', ' ').camelize trigger_human_name = trigger.to_s.tr('_', ' ').camelize
link_to path, rel: 'nofollow' do link_to path, rel: 'nofollow', method: :post do
content_tag(:span, trigger_human_name) content_tag(:span, trigger_human_name)
end end
end end
......
...@@ -116,7 +116,7 @@ module SnippetsHelper ...@@ -116,7 +116,7 @@ module SnippetsHelper
raw_project_snippet_url(@snippet.project, @snippet) raw_project_snippet_url(@snippet.project, @snippet)
end end
link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw' link_to external_snippet_icon('doc-code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
end end
def embedded_snippet_download_button def embedded_snippet_download_button
......
...@@ -124,7 +124,7 @@ class Notify < BaseMailer ...@@ -124,7 +124,7 @@ class Notify < BaseMailer
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= [] headers['References'] ||= []
headers['References'] << fallback_reply_message_id headers['References'].unshift(fallback_reply_message_id)
@reply_by_email = true @reply_by_email = true
end end
...@@ -158,7 +158,7 @@ class Notify < BaseMailer ...@@ -158,7 +158,7 @@ class Notify < BaseMailer
def mail_answer_thread(model, headers = {}) def mail_answer_thread(model, headers = {})
headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>" headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
headers['In-Reply-To'] = message_id(model) headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model) headers['References'] = [message_id(model)]
headers[:subject]&.prepend('Re: ') headers[:subject]&.prepend('Re: ')
......
...@@ -21,6 +21,14 @@ module Clusters ...@@ -21,6 +21,14 @@ module Clusters
end end
end end
def ready_status
[:installed]
end
def ready?
ready_status.include?(status_name)
end
def chart def chart
'stable/prometheus' 'stable/prometheus'
end end
......
...@@ -24,11 +24,10 @@ module PrometheusAdapter ...@@ -24,11 +24,10 @@ module PrometheusAdapter
def query(query_name, *args) def query(query_name, *args)
return unless can_query? return unless can_query?
query_class = Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query") query_class = query_klass_for(query_name)
query_args = build_query_args(*args)
args.map!(&:id) with_reactive_cache(query_class.name, *query_args, &query_class.method(:transform_reactive_result))
with_reactive_cache(query_class.name, *args, &query_class.method(:transform_reactive_result))
end end
# Cache metrics for specific environment # Cache metrics for specific environment
...@@ -44,5 +43,13 @@ module PrometheusAdapter ...@@ -44,5 +43,13 @@ module PrometheusAdapter
rescue Gitlab::PrometheusClient::Error => err rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err.message } { success: false, result: err.message }
end end
def query_klass_for(query_name)
Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query")
end
def build_query_args(*args)
args.map(&:id)
end
end end
end end
...@@ -59,6 +59,9 @@ module ReactiveCaching ...@@ -59,6 +59,9 @@ module ReactiveCaching
raise NotImplementedError raise NotImplementedError
end end
def reactive_cache_updated(*args)
end
def with_reactive_cache(*args, &blk) def with_reactive_cache(*args, &blk)
bootstrap = !within_reactive_cache_lifetime?(*args) bootstrap = !within_reactive_cache_lifetime?(*args)
Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
...@@ -81,8 +84,11 @@ module ReactiveCaching ...@@ -81,8 +84,11 @@ module ReactiveCaching
locking_reactive_cache(*args) do locking_reactive_cache(*args) do
if within_reactive_cache_lifetime?(*args) if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do enqueuing_update(*args) do
value = calculate_reactive_cache(*args) key = full_reactive_cache_key(*args)
Rails.cache.write(full_reactive_cache_key(*args), value) new_value = calculate_reactive_cache(*args)
old_value = Rails.cache.read(key)
Rails.cache.write(key, new_value)
reactive_cache_updated(*args) if new_value != old_value
end end
end end
end end
......
...@@ -90,34 +90,17 @@ module Routable ...@@ -90,34 +90,17 @@ module Routable
end end
def full_name def full_name
if route && route.name.present? route&.name || build_full_name
@full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
build_full_name
end
end end
# Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path,
# a new instance is instantiated, and we end up duplicating the same query to retrieve
# the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active? && persisted? route&.path || build_full_path
RequestStore[full_path_key] ||= uncached_full_path
end end
def full_path_components def full_path_components
full_path.split('/') full_path.split('/')
end end
def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active?
@full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def build_full_path def build_full_path
if parent && path if parent && path
parent.full_path + '/' + path parent.full_path + '/' + path
...@@ -138,16 +121,6 @@ module Routable ...@@ -138,16 +121,6 @@ module Routable
self.errors[:path].concat(route_path_errors) if route_path_errors self.errors[:path].concat(route_path_errors) if route_path_errors
end end
def uncached_full_path
if route && route.path.present?
@full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
build_full_path
end
end
def full_name_changed? def full_name_changed?
name_changed? || parent_changed? name_changed? || parent_changed?
end end
...@@ -156,10 +129,6 @@ module Routable ...@@ -156,10 +129,6 @@ module Routable
path_changed? || parent_changed? path_changed? || parent_changed?
end end
def full_path_key
@full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}"
end
def build_full_name def build_full_name
if parent && name if parent && name
parent.human_name + ' / ' + name parent.human_name + ' / ' + name
...@@ -168,18 +137,9 @@ module Routable ...@@ -168,18 +137,9 @@ module Routable
end end
end end
def update_route
return if Gitlab::Database.read_only?
prepare_route
route.save
end
def prepare_route def prepare_route
route || build_route(source: self) route || build_route(source: self)
route.path = build_full_path route.path = build_full_path
route.name = build_full_name route.name = build_full_name
@full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
@full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
end end
...@@ -11,8 +11,6 @@ module Storage ...@@ -11,8 +11,6 @@ module Storage
Namespace.find(parent_id_was) # raise NotFound early if needed Namespace.find(parent_id_was) # raise NotFound early if needed
end end
expires_full_path_cache
move_repositories move_repositories
if parent_changed? if parent_changed?
...@@ -34,13 +32,12 @@ module Storage ...@@ -34,13 +32,12 @@ module Storage
begin begin
send_update_instructions send_update_instructions
write_projects_repository_config write_projects_repository_config
rescue => e
true # Raise if development/test environment, else just notify Sentry
rescue Gitlab::Sentry.track_exception(e, extra: { full_path_was: full_path_was, full_path: full_path, action: 'move_dir' })
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
false
end end
true # false would cancel later callbacks but not rollback
end end
# Hooks # Hooks
......
class DeployToken < ActiveRecord::Base class DeployToken < ActiveRecord::Base
include Expirable include Expirable
include TokenAuthenticatable include TokenAuthenticatable
include PolicyActor
add_authentication_token_field :token add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
...@@ -58,10 +59,6 @@ class DeployToken < ActiveRecord::Base ...@@ -58,10 +59,6 @@ class DeployToken < ActiveRecord::Base
write_attribute(:expires_at, value.presence || Forever.date) write_attribute(:expires_at, value.presence || Forever.date)
end end
def admin?
false
end
private private
def ensure_at_least_one_scope def ensure_at_least_one_scope
......
...@@ -25,6 +25,10 @@ class Email < ActiveRecord::Base ...@@ -25,6 +25,10 @@ class Email < ActiveRecord::Base
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end end
def accept_pending_invitations!
user.accept_pending_invitations!
end
# once email is confirmed, update the gpg signatures # once email is confirmed, update the gpg signatures
def update_invalid_gpg_signatures def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed? user.update_invalid_gpg_signatures if confirmed?
......
...@@ -304,7 +304,6 @@ class Namespace < ActiveRecord::Base ...@@ -304,7 +304,6 @@ class Namespace < ActiveRecord::Base
def write_projects_repository_config def write_projects_repository_config
all_projects.find_each do |project| all_projects.find_each do |project|
project.expires_full_path_cache # we need to clear cache to validate renames correctly
project.write_repository_config project.write_repository_config
end end
end end
......
...@@ -31,6 +31,7 @@ class Project < ActiveRecord::Base ...@@ -31,6 +31,7 @@ class Project < ActiveRecord::Base
BoardLimitExceeded = Class.new(StandardError) BoardLimitExceeded = Class.new(StandardError)
STATISTICS_ATTRIBUTE = 'repositories_count'.freeze
NUMBER_OF_PERMITTED_BOARDS = 1 NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
# Hashed Storage versions handle rolling out new storage to project and dependents models: # Hashed Storage versions handle rolling out new storage to project and dependents models:
...@@ -79,6 +80,10 @@ class Project < ActiveRecord::Base ...@@ -79,6 +80,10 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! }
after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
after_create :create_ci_cd_settings, after_create :create_ci_cd_settings,
unless: :ci_cd_settings, unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? } if: proc { ProjectCiCdSetting.available? }
...@@ -1235,8 +1240,6 @@ class Project < ActiveRecord::Base ...@@ -1235,8 +1240,6 @@ class Project < ActiveRecord::Base
return true if skip_disk_validation return true if skip_disk_validation
return false unless repository_storage return false unless repository_storage
expires_full_path_cache # we need to clear cache to validate renames correctly
# Check if repository with same path already exists on disk we can # Check if repository with same path already exists on disk we can
# skip this for the hashed storage because the path does not change # skip this for the hashed storage because the path does not change
if legacy_storage? && repository_with_same_path_already_exists? if legacy_storage? && repository_with_same_path_already_exists?
...@@ -1615,7 +1618,6 @@ class Project < ActiveRecord::Base ...@@ -1615,7 +1618,6 @@ class Project < ActiveRecord::Base
# When we import a project overwriting the original project, there # When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions. # is a move operation. In that case we don't want to send the instructions.
send_move_instructions(full_path_was) unless import_started? send_move_instructions(full_path_was) unless import_started?
expires_full_path_cache
self.old_path_with_namespace = full_path_was self.old_path_with_namespace = full_path_was
SystemHooksService.new.execute_hooks_for(self, :rename) SystemHooksService.new.execute_hooks_for(self, :rename)
......
...@@ -19,6 +19,7 @@ class ProjectFeature < ActiveRecord::Base ...@@ -19,6 +19,7 @@ class ProjectFeature < ActiveRecord::Base
ENABLED = 20 ENABLED = 20
FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
STATISTICS_ATTRIBUTE = 'wikis_count'.freeze
class << self class << self
def access_level_attribute(feature) def access_level_attribute(feature)
...@@ -52,6 +53,9 @@ class ProjectFeature < ActiveRecord::Base ...@@ -52,6 +53,9 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
after_create ->(model) { SiteStatistic.track(STATISTICS_ATTRIBUTE) if model.wiki_enabled? }
after_update :update_site_statistics
def feature_available?(feature, user) def feature_available?(feature, user)
get_permission(user, access_level(feature)) get_permission(user, access_level(feature))
end end
...@@ -76,8 +80,30 @@ class ProjectFeature < ActiveRecord::Base ...@@ -76,8 +80,30 @@ class ProjectFeature < ActiveRecord::Base
issues_access_level > DISABLED issues_access_level > DISABLED
end end
# This is a workaround for the removal hooks not been triggered when removing a Project.
#
# ProjectFeature is removed using database cascade index rule.
# This method is called by Project model when deletion starts.
def untrack_statistics_for_deletion!
return unless wiki_enabled?
SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
end
private private
def update_site_statistics
return unless wiki_access_level_changed?
if self.wiki_access_level_was == DISABLED
# possible new states are PRIVATE / ENABLED, both should be tracked
SiteStatistic.track(STATISTICS_ATTRIBUTE)
elsif self.wiki_access_level == DISABLED
# old state was either PRIVATE / ENABLED, only untrack if new state is DISABLED
SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
end
end
# Validates builds and merge requests access level # Validates builds and merge requests access level
# which cannot be higher than repository access level # which cannot be higher than repository access level
def repository_children_level def repository_children_level
......
...@@ -48,13 +48,13 @@ class RemoteMirror < ActiveRecord::Base ...@@ -48,13 +48,13 @@ class RemoteMirror < ActiveRecord::Base
state :failed state :failed
after_transition any => :started do |remote_mirror, _| after_transition any => :started do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path) Gitlab::Metrics.add_event(:remote_mirrors_running)
remote_mirror.update(last_update_started_at: Time.now) remote_mirror.update(last_update_started_at: Time.now)
end end
after_transition started: :finished do |remote_mirror, _| after_transition started: :finished do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path) Gitlab::Metrics.add_event(:remote_mirrors_finished)
timestamp = Time.now timestamp = Time.now
remote_mirror.update!( remote_mirror.update!(
...@@ -63,7 +63,7 @@ class RemoteMirror < ActiveRecord::Base ...@@ -63,7 +63,7 @@ class RemoteMirror < ActiveRecord::Base
end end
after_transition started: :failed do |remote_mirror, _| after_transition started: :failed do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path) Gitlab::Metrics.add_event(:remote_mirrors_failed)
remote_mirror.update(last_update_at: Time.now) remote_mirror.update(last_update_at: Time.now)
end end
......
...@@ -1029,7 +1029,7 @@ class Repository ...@@ -1029,7 +1029,7 @@ class Repository
end end
def repository_event(event, tags = {}) def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags)) Gitlab::Metrics.add_event(event, tags)
end end
def initialize_raw_repository def initialize_raw_repository
......
class SiteStatistic < ActiveRecord::Base
# prevents the creation of multiple rows
default_value_for :id, 1
COUNTER_ATTRIBUTES = %w(repositories_count wikis_count).freeze
REQUIRED_SCHEMA_VERSION = 20180629153018
# Tracks specific attribute
#
# @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES
def self.track(raw_attribute)
with_statistics_available(raw_attribute) do |attribute|
SiteStatistic.update_all(["#{attribute} = #{attribute}+1"])
end
end
# Untracks specific attribute
#
# @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES
def self.untrack(raw_attribute)
with_statistics_available(raw_attribute) do |attribute|
SiteStatistic.update_all(["#{attribute} = #{attribute}-1 WHERE #{attribute} > 0"])
end
end
# Wrapper for track/untrack operations with basic validations and enforced requirements
#
# @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES
# @yield [String] attribute quoted to be used inside SQL / Arel query
def self.with_statistics_available(raw_attribute)
unless raw_attribute.in?(COUNTER_ATTRIBUTES)
raise ArgumentError, "Invalid attribute: '#{raw_attribute}' to '#{caller_locations(1, 1)[0].label}' method. " \
"Valid attributes are: #{COUNTER_ATTRIBUTES.join(', ')}"
end
return unless available?
self.fetch # make sure record exists
attribute = self.connection.quote_column_name(raw_attribute)
# will be running on its own transaction context
yield(attribute)
end
# Returns a site statistic record with tracked information
#
# @return [SiteStatistic] record with tracked information
def self.fetch
SiteStatistic.transaction(requires_new: true) do
SiteStatistic.first_or_create!
end
rescue ActiveRecord::RecordNotUnique
retry
end
# Return whether required schema change is available
#
# This is needed in order to degrade gracefully when testing schema migrations
#
# @return [Boolean] whether schema is available
def self.available?
@available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION
end
# Resets cached column information
#
# This is called during schema migration specs, in order to reset internal cache state
def self.reset_column_information
@available_flag = nil
super
end
end
# frozen_string_literal: true
# Include this module if we want to pass something else than the user to
# check policies. This defines several methods which the policy checker
# would call and check.
module PolicyActor
extend ActiveSupport::Concern
def blocked?
false
end
def admin?
false
end
def external?
false
end
def internal?
false
end
def access_locked?
false
end
def required_terms_not_accepted?
false
end
def can_create_group
false
end
end
...@@ -11,10 +11,15 @@ class PipelineSerializer < BaseSerializer ...@@ -11,10 +11,15 @@ class PipelineSerializer < BaseSerializer
:retryable_builds, :retryable_builds,
:cancelable_statuses, :cancelable_statuses,
:trigger_requests, :trigger_requests,
:project,
:manual_actions, :manual_actions,
:artifacts, :artifacts,
{ pending_builds: :project } {
pending_builds: :project,
project: [:route, { namespace: :route }],
artifacts: {
project: [:route, { namespace: :route }]
}
}
]) ])
end end
......
...@@ -50,17 +50,17 @@ module Clusters ...@@ -50,17 +50,17 @@ module Clusters
end end
def remove_installation_pod def remove_installation_pod
helm_api.delete_installation_pod!(install_command.pod_name) helm_api.delete_pod!(install_command.pod_name)
rescue rescue
# no-op # no-op
end end
def installation_phase def installation_phase
helm_api.installation_status(install_command.pod_name) helm_api.status(install_command.pod_name)
end end
def installation_errors def installation_errors
helm_api.installation_log(install_command.pod_name) helm_api.log(install_command.pod_name)
end end
end end
end end
......
...@@ -77,7 +77,6 @@ module Projects ...@@ -77,7 +77,6 @@ module Projects
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = @old_path project.old_path_with_namespace = @old_path
project.expires_full_path_cache
write_repository_config(@new_path) write_repository_config(@new_path)
......
...@@ -30,7 +30,7 @@ module Prometheus ...@@ -30,7 +30,7 @@ module Prometheus
return unless deployment_platform.respond_to?(:cluster) return unless deployment_platform.respond_to?(:cluster)
cluster = deployment_platform.cluster cluster = deployment_platform.cluster
return unless cluster.application_prometheus&.installed? return unless cluster.application_prometheus&.ready?
cluster.application_prometheus cluster.application_prometheus
end end
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
- if project.archived - if project.archived
%span.badge.badge-warning archived %span.badge.badge-warning archived
.title .title
= link_to [:admin, project.namespace.becomes(Namespace), project] do = link_to(admin_namespace_project_path(project.namespace, project)) do
.dash-project-avatar .dash-project-avatar
.avatar-container.s40 .avatar-container.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40') = project_icon(project, alt: '', class: 'avatar project-avatar s40')
......
- @body_class = 'ide' - @body_class = 'ide'
- page_title 'IDE' - page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'), #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'), "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'), "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
- site_name = "GitLab" - site_name = "GitLab"
%head{ prefix: "og: http://ogp.me/ns#" } %head{ prefix: "og: http://ogp.me/ns#" }
%meta{ charset: "utf-8" } %meta{ charset: "utf-8" }
- if Feature.enabled?('asset_host_prefetch') && ActionController::Base.asset_host
%link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host }
%link{ rel: 'preconnnect', href: ActionController::Base.asset_host, crossorigin: '' }
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' } %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
-# Open Graph - http://ogp.me/ -# Open Graph - http://ogp.me/
......
...@@ -67,5 +67,5 @@ ...@@ -67,5 +67,5 @@
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only= _("Toggle navigation") %span.sr-only= _("Toggle navigation")
= sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
= link_to group_path(@group) do = link_to group_path(@group) do
.nav-icon-container .nav-icon-container
= sprite_icon('project') = sprite_icon('home')
%span.nav-item-name %span.nav-item-name
= _('Overview') = _('Overview')
......
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
= nav_link(controller: :gpg_keys) do = nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path do = link_to profile_gpg_keys_path do
.nav-icon-container .nav-icon-container
= sprite_icon('key-2') = sprite_icon('key-modern')
%span.nav-item-name %span.nav-item-name
= _('GPG Keys') = _('GPG Keys')
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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