Commit 5d1d5830 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-temp-file-folder-fixes

parents 356a37cf 54e26296
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
......
......@@ -82,7 +82,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gitlab-gollum-lib', '~> 4.2'
gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
......@@ -140,7 +140,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 2.0'
gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
......@@ -415,7 +415,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......@@ -434,5 +434,3 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
gem 'goldiloader', '~> 2.0'
......@@ -290,9 +290,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.94.0)
gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
......@@ -303,12 +303,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.1)
gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
......@@ -331,9 +331,6 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.1.0)
......@@ -750,7 +747,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.2.1)
rouge (3.1.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......@@ -1064,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0)
gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......@@ -1072,7 +1069,6 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
......@@ -1164,7 +1160,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
......
......@@ -69,7 +69,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.100)
attr_encrypted (3.0.3)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
......@@ -291,9 +291,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.94.0)
gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
......@@ -304,6 +304,17 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.1)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
......@@ -321,22 +332,8 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.2.7)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
......@@ -1009,7 +1006,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
......@@ -1069,15 +1066,14 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0)
gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
export default {
components: {
icon,
Icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
showTooltip: {
type: Boolean,
required: false,
default: false,
},
showStagedIcon: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
changedIcon() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`;
},
stagedIcon() {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
return `multi-${this.changedIcon}`;
return `multi-${this.changedIcon} prepend-left-5 pull-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
const type = this.file.tempFile ? 'addition' : 'modification';
if (this.file.changed && !this.file.staged) {
return sprintf(__('Unstaged %{type}'), {
type,
});
} else if (!this.file.changed && this.file.staged) {
return sprintf(__('Staged %{type}'), {
type,
});
} else if (this.file.changed && this.file.staged) {
return sprintf(__('Unstaged and staged %{type}'), {
type: pluralize(type),
});
}
return undefined;
},
},
};
</script>
<template>
<span
v-tooltip
:title="tooltipTitle"
data-container="body"
data-placement="right"
class="ide-file-changed-icon"
>
<icon
v-if="file.staged && showStagedIcon"
:name="stagedIcon"
:size="12"
:css-classes="changedIconClass"
/>
<icon
v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
:name="changedIcon"
:size="12"
:css-classes="`ide-file-changed-icon ${changedIconClass}`"
:css-classes="changedIconClass"
/>
</span>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<button
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<div
class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
>
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default {
export default {
components: {
icon,
listItem,
listCollapsed,
Icon,
ListItem,
ListCollapsed,
},
directives: {
tooltip,
},
props: {
title: {
......@@ -19,38 +24,109 @@
type: Array,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: true,
},
iconName: {
type: String,
required: true,
},
action: {
type: String,
required: true,
},
actionBtnText: {
type: String,
required: true,
},
itemActionComponent: {
type: String,
required: true,
},
stagedList: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
isCommitInfoShown() {
return this.rightPanelCollapsed || this.fileList.length;
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
});
},
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
},
};
};
</script>
<template>
<div
class="ide-commit-list-container"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<header
class="multi-file-commit-panel-header"
>
<div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
:class="{
'multi-file-commit-list': isCommitInfoShown
'append-right-10': showToggle,
}"
>
<icon
v-once
:name="iconName"
:size="18"
/>
{{ titleText }}
<button
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
>
{{ actionBtnText }}
</button>
</div>
<button
v-if="showToggle"
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<list-collapsed
v-if="rightPanelCollapsed"
:files="fileList"
:icon-name="iconName"
:title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
......@@ -58,9 +134,18 @@
>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</template>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { sprintf, n__, __ } from '~/locale';
export default {
export default {
components: {
icon,
Icon,
},
directives: {
tooltip,
},
props: {
files: {
type: Array,
required: true,
},
iconName: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
computed: {
...mapGetters([
'addedFiles',
'modifiedFiles',
]),
addedFilesLength() {
return this.files.filter(f => f.tempFile).length;
},
modifiedFilesLength() {
return this.files.filter(f => !f.tempFile).length;
},
addedFilesIconClass() {
return this.addedFilesLength ? 'multi-file-addition' : '';
},
modifiedFilesClass() {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
});
},
modifiedTooltip() {
return sprintf(
n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
{ type: this.title.toLowerCase() },
);
},
titleTooltip() {
return sprintf(__('%{title} changes'), { title: this.title });
},
additionIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
},
modifiedIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
},
};
},
};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
<div
v-tooltip
:title="titleTooltip"
data-container="body"
data-placement="left"
class="append-bottom-15"
>
<icon
name="file-addition"
v-once
:name="iconName"
:size="18"
css-classes="multi-file-addition append-bottom-10"
/>
{{ addedFiles.length }}
</div>
<div
v-tooltip
:title="additionsTooltip"
data-container="body"
data-placement="left"
class="append-bottom-10"
>
<icon
name="file-modified"
:name="additionIconName"
:size="18"
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
:css-classes="addedFilesIconClass"
/>
{{ modifiedFiles.length }}
</div>
{{ addedFilesLength }}
<div
v-tooltip
:title="modifiedTooltip"
data-container="body"
data-placement="left"
class="prepend-top-10 append-bottom-10"
>
<icon
:name="modifiedIconName"
:size="18"
:css-classes="modifiedFilesClass"
/>
</div>
{{ modifiedFilesLength }}
</div>
</template>
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
export default {
components: {
Icon,
StageButton,
UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
actionComponent: {
type: String,
required: true,
},
keyPrefix: {
type: String,
required: false,
default: '',
},
stagedList: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
const prefix = this.stagedList ? '-solid' : '';
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
},
},
methods: {
...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
openFileInEditor(file) {
return this.openPendingTab(file).then(changeViewer => {
...mapActions([
'discardFileChanges',
'updateViewer',
'openPendingTab',
'unstageChange',
'stageChange',
]),
openFileInEditor() {
return this.openPendingTab({
file: this.file,
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
});
},
fileAction() {
if (this.file.staged) {
this.unstageChange(this.file.path);
} else {
this.stageChange(this.file.path);
}
},
},
};
</script>
......@@ -38,7 +73,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
@click="openFileInEditor(file)">
@dblclick="fileAction"
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
......@@ -47,12 +84,9 @@ export default {
/>{{ file.path }}
</span>
</button>
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file.path)"
>
Discard
</button>
<component
:is="actionComponent"
:path="file.path"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions(['stageChange', 'discardFileChanges']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
v-tooltip
type="button"
class="btn btn-blank append-right-5"
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
data-container="body"
@click.stop="stageChange(path)"
>
<icon
name="mobile-issue-close"
:size="12"
/>
</button>
<button
v-tooltip
type="button"
class="btn btn-blank"
:aria-label="__('Discard changes')"
:title="__('Discard changes')"
data-container="body"
@click.stop="discardFileChanges(path)"
>
<icon
name="remove"
:size="12"
/>
</button>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions(['unstageChange']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
v-tooltip
type="button"
class="btn btn-blank"
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
data-container="body"
@click="unstageChange(path)"
>
<icon
name="history"
:size="12"
/>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
......@@ -22,13 +21,6 @@ export default {
required: true,
},
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
......@@ -41,40 +33,6 @@ export default {
<div
class="multi-file-commit-panel-section"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed"
>
<div
v-if="changedFiles.length"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
......
......@@ -22,13 +22,6 @@ export default {
<template>
<div class="ide-status-bar">
<div class="ref-name">
<icon
name="branch"
:size="12"
/>
{{ file.branchId }}
</div>
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
......@@ -12,8 +13,9 @@ import Actions from './commit_sidebar/actions.vue';
export default {
components: {
DeprecatedModal,
icon,
commitFilesList,
Icon,
CommitFilesList,
EmptyState,
Actions,
LoadingButton,
CommitMessageField,
......@@ -32,33 +34,17 @@ export default {
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
'lastCommitMsg',
'changedFiles',
]),
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
......@@ -69,9 +55,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
:class="{
'multi-file-commit-empty-state-container': !changedFiles.length
}"
>
<deprecated-modal
id="ide-create-branch-modal"
......@@ -85,15 +68,27 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
<template
v-if="changedFiles.length || stagedFiles.length"
>
<commit-files-list
title="Staged"
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
action="stageAllChanges"
:action-btn-text="__('Stage all')"
item-action-component="stage-button"
/>
<commit-files-list
icon-name="staged"
:title="__('Staged')"
:file-list="stagedFiles"
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true"
/>
<template
v-if="changedFiles.length"
>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
......@@ -123,38 +118,10 @@ export default {
</div>
</form>
</template>
<div
v-else-if="!rightPanelCollapsed"
class="row js-empty-state"
>
<div class="col-xs-10 col-xs-offset-1">
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
</div>
<div class="col-xs-10 col-xs-offset-1">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
<empty-state
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg">
</p>
</div>
</div>
</div>
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</template>
......@@ -20,7 +20,7 @@ export default {
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']),
...mapGetters(['currentMergeRequest', 'getStagedFile']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
......@@ -120,7 +120,12 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.file);
const head = this.getStagedFile(this.file.path);
this.model = this.editor.createModel(
this.file,
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
......
......@@ -102,8 +102,11 @@ export default {
v-if="file.mrChange"
/>
<changed-file-icon
v-if="file.changed || file.tempFile || file.staged"
:file="file"
v-if="file.changed || file.tempFile"
:show-tooltip="true"
:show-staged-icon="true"
class="prepend-top-5 pull-right"
/>
</span>
<new-dropdown
......
......@@ -26,13 +26,16 @@ export default {
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
return this.tab.changed ? !this.tabMouseOver : false;
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
......@@ -42,18 +45,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
this.openPendingTab(tab);
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
if (this.tab.changed) {
if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
if (this.tab.changed) {
if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
......
......@@ -3,15 +3,16 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
constructor(monaco, file) {
constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
this.file.raw,
head ? head.content : this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
......@@ -34,10 +35,12 @@ export default class Model {
this.events = new Map();
this.updateContent = this.updateContent.bind(this);
this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
......@@ -79,8 +82,15 @@ export default class Model {
);
}
updateContent(content) {
updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
if (!changed) {
this.getModel().setValue(content);
}
}
updateNewContent(content) {
this.getModel().setValue(content);
}
......@@ -89,6 +99,7 @@ export default class Model {
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}
......@@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
addModel(file) {
addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
const model = new Model(this.monaco, file);
const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);
......
......@@ -77,8 +77,8 @@ export default class Editor {
}
}
createModel(file) {
return this.modelManager.addModel(file);
createModel(file, head = null) {
return this.modelManager.addModel(file, head);
}
attachModel(model) {
......
import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
......@@ -32,6 +33,22 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
export const toggleRightPanelCollapsed = (
{ dispatch, state },
e = undefined,
) => {
if (e) {
$(e.currentTarget)
.tooltip('hide')
.blur();
}
dispatch('setPanelCollapsedStatus', {
side: 'right',
collapsed: !state.rightPanelCollapsed,
});
};
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
......@@ -108,6 +125,14 @@ export const scrollToTab = () => {
});
};
export const stageAllChanges = ({ state, commit }) => {
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
};
export const unstageAllChanges = ({ state, commit }) => {
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
};
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
......
......@@ -24,7 +24,10 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
dispatch('openPendingTab', nextFileToOpen);
dispatch('openPendingTab', {
file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
......@@ -153,7 +156,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
export const discardFileChanges = ({ state, commit }, path) => {
export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
......@@ -161,17 +164,40 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
} else if (getters.activeFile && file.path === getters.activeFile.path) {
dispatch('updateDelayViewerUpdated', true)
.then(() => {
router.push(`/project${file.url}`);
})
.catch(e => {
throw e;
});
}
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
};
export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
};
export const unstageChange = ({ commit }, path) => {
commit(types.UNSTAGE_CHANGE, path);
};
export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
commit(types.ADD_PENDING_TAB, { file });
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
......
import { __ } from '~/locale';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
......@@ -29,9 +31,15 @@ export const currentMergeRequest = state => {
};
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId;
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
......@@ -98,23 +98,14 @@ export const updateFilesAfterCommit = (
{ root: true },
);
rootState.changedFiles.forEach(entry => {
commit(
rootTypes.SET_LAST_COMMIT_DATA,
{
entry,
lastCommit,
},
{ root: true },
);
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
rootState.stagedFiles.forEach(file => {
const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
rootTypes.SET_FILE_RAW_DATA,
rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
file: entry,
raw: entry.content,
file,
lastCommit,
},
{ root: true },
);
......@@ -122,18 +113,21 @@ export const updateFilesAfterCommit = (
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
file: entry,
file,
changed: false,
},
{ root: true },
);
dispatch('updateTempFlagForEntry', { entry, tempFile: false }, { root: true });
});
dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content,
changed: !!changedFile,
});
});
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
......@@ -186,6 +180,8 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
{ root: true },
);
}
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})
......
import * as consts from './constants';
export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
const BRANCH_SUFFIX_COUNT = 5;
export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
-BRANCH_SUFFIX_COUNT,
)}`;
export const branchName = (state, getters, rootState) => {
if (
......
......@@ -51,6 +51,11 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
......
......@@ -50,6 +50,11 @@ export default {
lastCommitMsg,
});
},
[types.CLEAR_STAGED_CHANGES](state) {
Object.assign(state, {
stagedFiles: [],
});
},
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
......@@ -102,6 +107,22 @@ export default {
delayViewerUpdated,
});
},
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path);
Object.assign(state.entries[file.path], {
raw: file.content,
changed: !!changedFile,
staged: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
}),
});
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,
......
......@@ -57,7 +57,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
const stagedFile = state.stagedFiles.find(f => f.path === path);
const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
......@@ -91,8 +93,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
Object.assign(state.entries[path], {
content: state.entries[path].raw,
content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
......@@ -106,16 +110,67 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.STAGE_CHANGE](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: true,
changed: false,
}),
}),
});
if (stagedFile) {
Object.assign(stagedFile, {
...state.entries[path],
});
} else {
Object.assign(state, {
stagedFiles: state.stagedFiles.concat({
...state.entries[path],
}),
});
}
},
[types.UNSTAGE_CHANGE](state, path) {
const changedFile = state.changedFiles.find(f => f.path === path);
const stagedFile = state.stagedFiles.find(f => f.path === path);
if (!changedFile && stagedFile) {
Object.assign(state.entries[path], {
...stagedFile,
key: state.entries[path].key,
active: state.entries[path].active,
opened: state.entries[path].opened,
changed: true,
});
Object.assign(state, {
changedFiles: state.changedFiles.concat(state.entries[path]),
});
}
Object.assign(state, {
stagedFiles: state.stagedFiles.filter(f => f.path !== path),
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: false,
}),
}),
});
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
let openFiles = state.openFiles.map(f =>
Object.assign(f, { active: f.path === file.path, opened: false }),
);
const key = `${keyPrefix}-${file.key}`;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
......@@ -126,10 +181,11 @@ export default {
if (f.path === file.path) {
return acc.concat({
...f,
content: file.content,
active: true,
pending: true,
opened: true,
key: `${keyPrefix}-${f.key}`,
key,
});
}
......
......@@ -3,6 +3,7 @@ export default () => ({
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
......
......@@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
......@@ -104,7 +105,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
actions: rootState.changedFiles.map(f => ({
actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
......
......@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
$block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
$block.find('.value').hide();
$block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
......
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import callout from '../../vue_shared/components/callout.vue';
export default {
export default {
name: 'JobHeaderSection',
components: {
ciHeader,
loadingIcon,
callout,
},
props: {
job: {
......@@ -30,6 +32,9 @@
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
shouldRenderReason() {
return !!(this.job.status && this.job.callout_message);
},
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
......@@ -58,9 +63,10 @@
return actions;
},
},
};
};
</script>
<template>
<header>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
......@@ -79,4 +85,10 @@
class="prepend-top-default append-bottom-default"
/>
</div>
<callout
v-if="shouldRenderReason"
:message="job.callout_message"
/>
</header>
</template>
<script>
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
export default {
export default {
name: 'SidebarDetailsBlock',
components: {
detailRow,
loadingIcon,
},
mixins: [
timeagoMixin,
],
mixins: [timeagoMixin],
props: {
job: {
type: Object,
......@@ -22,6 +20,11 @@
type: Boolean,
required: true,
},
canUserRetry: {
type: Boolean,
required: false,
default: false,
},
runnerHelpUrl: {
type: String,
required: false,
......@@ -44,6 +47,14 @@
runnerId() {
return `#${this.job.runner.id}`;
},
retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
className +=
this.job.status && this.job.recoverable
? ' btn-primary'
: ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
......@@ -60,7 +71,8 @@
return t;
},
renderBlock() {
return this.job.merge_request ||
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
......@@ -68,13 +80,40 @@
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
this.job.cancel_path
);
},
},
};
};
</script>
<template>
<div>
<div class="block">
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
<a
v-if="canUserRetry"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
<button
type="button"
:aria-label="__('Toggle Sidebar')"
class="btn btn-blank gutter-toggle pull-right
visible-xs-block visible-sm-block js-sidebar-build-toggle"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
......@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
New issue
{{ __('New issue') }}
</a>
<a
v-if="job.retry_path"
v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
Retry
{{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
......@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
Merge Request:
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
......@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
Tags:
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
......@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
Cancel
{{ __('Cancel') }}
</a>
</div>
</div>
......
......@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
const detailsBlockElement = document.getElementById('js-details-block-vue');
const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
el: '#js-details-block-vue',
el: detailsBlockElement,
components: {
detailsBlock,
},
......@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
......
<script>
const calloutVariants = ['danger', 'success', 'info', 'warning'];
export default {
props: {
category: {
type: String,
required: false,
default: calloutVariants[0],
validator: value => calloutVariants.includes(value),
},
message: {
type: String,
required: true,
},
},
};
</script>
<template>
<div
:class="`bs-callout bs-callout-${category}`"
role="alert"
aria-live="assertive"
>
{{ message }}
</div>
</template>
......@@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
transition-duration: .3s;
transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
......@@ -137,6 +137,12 @@
}
}
.issuable-sidebar .labels {
.value.dont-hide ~ .selectbox {
padding-top: $gl-padding-8;
}
}
.pikaday-container {
.pika-single {
margin-top: 2px;
......@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
@keyframes fade-out-status {
0%, 50% { opacity: 1; }
100% { opacity: 0; }
0%,
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes blinking-dots {
......@@ -30,10 +36,21 @@
}
@keyframes blinking-scroll-button {
0% { opacity: 0.2; }
25% { opacity: 0.5; }
50% { opacity: 0.7; }
100% { opacity: 1; }
0% {
opacity: 0.2;
}
25% {
opacity: 0.5;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.build-page {
......@@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s;
animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s;
animation-delay: 0.2s;
}
.third-triangle {
......
......@@ -74,6 +74,10 @@
.ide-file-changed-icon {
margin-left: auto;
> svg {
display: block;
}
}
.ide-new-btn {
......@@ -382,7 +386,11 @@
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
justify-content: space-between;
justify-content: flex-end;
> div + div {
padding-left: $gl-padding;
}
svg {
vertical-align: middle;
......@@ -525,9 +533,13 @@
overflow: auto;
}
.multi-file-commit-empty-state-container {
align-items: center;
justify-content: center;
.ide-commit-empty-state {
padding: 0 $gl-padding;
}
.ide-commit-empty-state-container {
margin-top: auto;
margin-bottom: auto;
}
.multi-file-commit-panel-header {
......@@ -536,35 +548,22 @@
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
&.is-collapsed {
border-bottom: 1px solid $white-dark;
svg {
margin-left: auto;
margin-right: auto;
}
.multi-file-commit-panel-collapse-btn {
margin-right: auto;
margin-left: auto;
border-left: 0;
}
}
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
padding: 0 $gl-btn-padding;
padding-left: $grid-size;
svg {
margin-right: $gl-btn-padding;
color: $theme-gray-700;
}
}
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
margin-left: auto;
}
.multi-file-commit-list {
......@@ -578,12 +577,14 @@
display: flex;
padding: 0;
align-items: center;
border-radius: $border-radius-default;
.multi-file-discard-btn {
display: none;
margin-top: -2px;
margin-left: auto;
margin-right: $grid-size;
color: $gl-link-color;
padding: 0 2px;
&:focus,
&:hover {
......@@ -595,26 +596,31 @@
background: $white-normal;
.multi-file-discard-btn {
display: block;
display: flex;
}
}
}
.multi-file-addition {
.multi-file-additions,
.multi-file-additions-solid {
fill: $green-500;
}
.multi-file-modified {
.multi-file-modified,
.multi-file-modified-solid {
fill: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
padding: $gl-padding 0;
> svg {
svg {
display: block;
margin-left: auto;
margin-right: auto;
color: $theme-gray-700;
}
.file-status-icon {
......@@ -626,7 +632,7 @@
.multi-file-commit-list-path {
padding: $grid-size / 2;
padding-left: $gl-padding;
padding-left: $grid-size;
background: none;
border: 0;
text-align: left;
......@@ -811,6 +817,41 @@
}
}
.ide-commit-list-container {
display: flex;
flex-direction: column;
width: 100%;
padding: 0 16px;
&:not(.is-collapsed) {
flex: 1;
min-height: 140px;
}
&.is-collapsed {
.multi-file-commit-panel-header {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
svg {
margin-left: auto;
margin-right: auto;
}
.multi-file-commit-panel-collapse-btn {
margin-right: auto;
margin-left: auto;
border-left: 0;
}
}
}
}
.ide-staged-action-btn {
margin-left: auto;
color: $gl-link-color;
}
.ide-commit-radios {
label {
font-weight: normal;
......
......@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h)
end
result[:html] = result[:html].presence || 'No job log'
render json: result
end
end
......
......@@ -29,8 +29,7 @@ class Projects::WikisController < Projects::ApplicationController
else
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
@page = build_page(title: params[:id])
render 'edit'
end
......@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
......@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController
else
render action: "edit"
end
rescue Gitlab::Git::Wiki::OperationError => e
@page = build_page(wiki_params)
@error = e
render 'edit'
end
def history
......@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to project_wiki_path(@project, :home),
status: 302,
notice: "Page was successfully deleted"
rescue Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
def git_access
......@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
def build_page(args)
WikiPage.new(@project_wiki).tap do |page|
page.update_attributes(args)
end
end
end
......@@ -134,10 +134,8 @@ class GroupDescendantsFinder
end
def direct_child_projects
GroupProjectsFinder.new(group: parent_group,
current_user: current_user,
options: { only_owned: true },
params: params).execute
GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
.execute
end
# Finds all projects nested under `parent_group` or any of its descendant
......
......@@ -28,7 +28,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
......
......@@ -20,7 +20,7 @@ module Ci
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
......@@ -95,8 +95,8 @@ module Ci
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
after_commit :update_project_statistics_after_save, on: [:create, :update]
after_commit :update_project_statistics, on: :destroy
after_save :update_project_statistics_after_save, if: :artifacts_size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
class << self
# This is needed for url_for to work,
......@@ -611,7 +611,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(','))
variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
......@@ -664,16 +664,20 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
def update_project_statistics
return unless project
def update_project_statistics_after_save
update_project_statistics(read_attribute(:artifacts_size).to_i - artifacts_size_was.to_i)
end
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
def update_project_statistics_after_destroy
update_project_statistics(-artifacts_size)
end
def update_project_statistics_after_save
if previous_changes.include?('artifacts_size')
update_project_statistics
def update_project_statistics(difference)
ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
end
def project_destroyed?
project.pending_delete?
end
end
end
......@@ -7,12 +7,15 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
before_save :update_file_store
mount_uploader :file, JobArtifactUploader
before_save :set_size, if: :file_changed?
after_save :update_project_statistics_after_save, if: :size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
after_save :update_file_store
mount_uploader :file, JobArtifactUploader
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
delegate :exists?, :open, to: :file
......@@ -23,7 +26,9 @@ module Ci
}
def update_file_store
self.file_store = file.object_store
# The file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
self.update_column(:file_store, file.object_store)
end
def self.artifacts_size_for(project)
......@@ -34,10 +39,6 @@ module Ci
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
def set_size
self.size = file.size
end
def expire_in
expire_at - Time.now if expire_at
end
......@@ -48,5 +49,28 @@ module Ci
ChronicDuration.parse(value)&.seconds&.from_now
end
end
private
def set_size
self.size = file.size
end
def update_project_statistics_after_save
update_project_statistics(size.to_i - size_was.to_i)
end
def update_project_statistics_after_destroy
update_project_statistics(-self.size)
end
def update_project_statistics(difference)
ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
end
def project_destroyed?
# Use job.project to avoid extra DB query for project
job.project.pending_delete?
end
end
end
......@@ -13,7 +13,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { auto_include(false) }, through: :runner_projects
has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
......
......@@ -15,7 +15,7 @@ module Clusters
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, -> { auto_include(false) }, through: :cluster_projects, class_name: '::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
......
......@@ -37,7 +37,20 @@ module GroupDescendant
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
if parent.nil? && !child.parent_id.nil?
raise ArgumentError.new('parent was not preloaded')
parent = child.parent
exception = ArgumentError.new <<~MSG
parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]")
This error is not user facing, but causes a +1 query.
MSG
extras = {
parent: parent,
child: child,
preloaded: preloaded.map(&:full_path)
}
issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
end
if parent.nil? && hierarchy_top.present?
......
......@@ -48,7 +48,7 @@ module Issuable
end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, -> { auto_include(false) }, through: :label_links
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
......
......@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
@notes = notes_relation.fresh.auto_include(false).to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
@notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |name|
clear_memoization(name)
......
......@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { auto_include(false) }, through: :deploy_keys_projects
has_many :projects, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) }
......
......@@ -8,7 +8,7 @@ class DeployToken < ActiveRecord::Base
default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token
has_many :projects, -> { auto_include(false) }, through: :project_deploy_tokens
has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
before_save :ensure_token
......
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
has_many :projects, -> { auto_include(false) }, through: :fork_network_members
has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
......
......@@ -12,9 +12,9 @@ class Group < Namespace
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
has_many :users, -> { auto_include(false) }, through: :group_members
has_many :users, through: :group_members
has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) },
-> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
......@@ -23,7 +23,7 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, -> { auto_include(false) }, through: :project_group_links, source: :project
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
......
......@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :assignees, -> { auto_include(false) }, class_name: "User", through: :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true
......
......@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'MergeRequest'
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color
......
......@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { auto_include(false) }, through: :lfs_objects_projects
has_many :projects, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
......@@ -11,10 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
before_save :update_file_store
after_save :update_file_store
def update_file_store
self.file_store = file.object_store
# The file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
self.update_column(:file_store, file.object_store)
end
def project_allowed_access?(project)
......
......@@ -22,7 +22,7 @@ class Milestone < ActiveRecord::Base
belongs_to :group
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title').auto_include(false) }, through: :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
......@@ -248,10 +248,6 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
def features
[]
end
def refresh_project_authorizations
owner.refresh_authorized_projects
end
......
......@@ -139,10 +139,10 @@ class Project < ActiveRecord::Base
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, -> { auto_include(false) }, through: :forked_project_link
has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, -> { auto_include(false) }, through: :forked_project_links, source: :forked_to_project
has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
......@@ -150,7 +150,7 @@ class Project < ActiveRecord::Base
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
has_one :fork_network, -> { auto_include(false) }, through: :fork_network_member
has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
......@@ -167,27 +167,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags
has_many :project_authorizations
has_many :authorized_users, -> { auto_include(false) }, through: :project_authorizations, source: :user, class_name: 'User'
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
has_many :users, -> { auto_include(false) }, through: :project_members
has_many :users, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
has_many :deploy_keys, -> { auto_include(false) }, through: :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects
has_many :starrers, -> { auto_include(false) }, through: :users_star_projects, source: :user
has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :lfs_objects, -> { auto_include(false) }, through: :lfs_objects_projects
has_many :lfs_objects, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
has_many :invited_groups, -> { auto_include(false) }, through: :project_group_links, source: :group
has_many :invited_groups, through: :project_group_links, source: :group
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
......@@ -199,7 +199,7 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, -> { auto_include(false) }, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
......@@ -216,16 +216,16 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, -> { auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, -> { auto_include(false) }, through: :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
has_many :active_runners, -> { active.auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
......@@ -1875,6 +1875,10 @@ class Project < ActiveRecord::Base
memoized_results[cache_key]
end
def licensed_features
[]
end
private
def storage
......
......@@ -4,15 +4,15 @@ class ProjectStatistics < ActiveRecord::Base
before_save :update_storage_size
STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
INCREMENTABLE_COLUMNS = [:build_artifacts_size].freeze
def total_repository_size
repository_size + lfs_objects_size
end
def refresh!(only: nil)
STATISTICS_COLUMNS.each do |column, generator|
COLUMNS_TO_REFRESH.each do |column, generator|
if only.blank? || only.include?(column)
public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
end
......@@ -34,13 +34,15 @@ class ProjectStatistics < ActiveRecord::Base
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
def update_build_artifacts_size
self.build_artifacts_size =
project.builds.sum(:artifacts_size) +
Ci::JobArtifact.artifacts_size_for(self.project)
def update_storage_size
self.storage_size = repository_size + lfs_objects_size + build_artifacts_size
end
def update_storage_size
self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
def self.increment_statistic(project_id, key, amount)
raise ArgumentError, "Cannot increment attribute: #{key}" unless key.in?(INCREMENTABLE_COLUMNS)
return if amount == 0
where(project_id: project_id)
.update_all(["#{key} = COALESCE(#{key}, 0) + (?)", amount])
end
end
......@@ -179,7 +179,11 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
Gitlab::Git::Wiki::CommitDetails.new(@user.id,
@user.username,
@user.name,
@user.email,
commit_message)
end
def default_message(action, title)
......
......@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
belongs_to :target, -> { auto_include(false) }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
......
......@@ -96,23 +96,23 @@ class User < ActiveRecord::Base
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
has_many :groups, -> { auto_include(false) }, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group
has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }).auto_include(false) }, through: :group_members, source: :group
has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
# Projects
has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects
has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, -> { auto_include(false) }, through: :project_members
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :starred_projects, -> { auto_include(false) }, through: :users_star_projects, source: :project
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :project_authorizations
has_many :authorized_projects, -> { auto_include(false) }, through: :project_authorizations, source: :project
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
has_many :project_interactions, -> { auto_include(false) }, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
......@@ -132,7 +132,7 @@ class User < ActiveRecord::Base
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :assigned_issues, -> { auto_include(false) }, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
......
......@@ -265,6 +265,15 @@ class WikiPage
title.present? && self.class.unhyphenize(@page.url_path) != title
end
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
end
private
# Process and format the title based on the user input.
......@@ -290,15 +299,6 @@ class WikiPage
File.join(components)
end
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
end
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
......
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
}.freeze
presents :build
def erased_by_user?
......@@ -35,6 +44,14 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
private
def tooltip_for_badge
......@@ -44,5 +61,9 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end
end
......@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
expose :callout_message, if: -> (*) { failed? }
expose :recoverable, if: -> (*) { failed? }
private
......@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end
def failed?
build.failed?
end
def callout_message
build_presenter.callout_failure_message
end
def recoverable
build_presenter.recoverable?
end
def build_presenter
@build_presenter ||= build.present
end
end
......@@ -33,7 +33,7 @@ module Ci
end
end
builds.auto_include(false).find do |build|
builds.find do |build|
next unless runner.can_pick?(build)
begin
......
......@@ -64,9 +64,14 @@ module Labels
end
def update_label_links(labels, old_label_id:, new_label_id:)
LabelLink.joins(:label)
.merge(labels)
.where(label_id: old_label_id)
# use 'labels' relation to get label_link ids only of issues/MRs
# in the project being transferred.
# IDs are fetched in a separate query because MySQL doesn't
# allow referring of 'label_links' table in UPDATE query:
# https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/62435068
link_ids = labels.pluck('label_links.id')
LabelLink.where(id: link_ids, label_id: old_label_id)
.update_all(label_id: new_label_id)
end
......
......@@ -6,10 +6,10 @@ class JobArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
def size
return super if model.size.nil?
def cached_size
return model.size if model.size.present? && !model.file_changed?
model.size
size
end
def store_dir
......@@ -20,7 +20,7 @@ class JobArtifactUploader < GitlabUploader
if file_storage?
File.open(path, "rb") if path
else
::Gitlab::Ci::Trace::HttpIO.new(url, size) if url
::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url
end
end
......
......@@ -183,14 +183,6 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
def default_object_store
if self.object_store_enabled? && self.direct_upload_enabled?
Store::REMOTE
else
Store::LOCAL
end
end
end
# allow to configure and overwrite the filename
......@@ -211,12 +203,13 @@ module ObjectStorage
end
def object_store
@object_store ||= model.try(store_serialization_column) || self.class.default_object_store
# We use Store::LOCAL as null value indicates the local storage
@object_store ||= model.try(store_serialization_column) || Store::LOCAL
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
@object_store = value || self.class.default_object_store
@object_store = value || Store::LOCAL
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
......@@ -302,6 +295,15 @@ module ObjectStorage
super
end
def store!(new_file = nil)
# when direct upload is enabled, always store on remote storage
if self.class.object_store_enabled? && self.class.direct_upload_enabled?
self.object_store = Store::REMOTE
end
super
end
private
def schedule_background_upload?
......
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
.block
%strong.inline.prepend-top-8
= @build.name
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
= icon('angle-double-right')
#js-details-block-vue
#js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block
......
......@@ -4,7 +4,7 @@
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels
.value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None
%a{ href: "#",
......
......@@ -96,7 +96,7 @@
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
......
---
title: Keep current labels visible when editing them in the sidebar
merge_request:
author:
type: changed
---
title: Improve DB performance of calculating total artifacts size
merge_request: 17839
author:
type: performance
---
title: Remove branch name from the status bar of WebIDE
merge_request:
author:
type: changed
---
title: Show shared projects on group page
merge_request: 18390
author:
type: fixed
---
title: Fix direct_upload when records with null file_store are used
merge_request:
author:
type: fixed
---
title: Add Goldiloader to fix N+1 issues when calculating email recipients
merge_request:
author:
type: performance
---
title: Triggering custom hooks by Wiki UI edit
merge_request: 18251
author:
type: fixed
---
title: Fix label links update on project transfer
merge_request:
author:
type: fixed
# This is a monkey patch which must be removed when migrating to Rails 5.1 from 5.0.
#
# In Rails 5.0 there was introduced a bug which casts types in the uniqueness validator.
# https://github.com/rails/rails/pull/23523/commits/811a4fa8eb6ceea841e61e8ac05747ffb69595ae
#
# That causes to bugs like this:
#
# 1) API::Users POST /user/:id/gpg_keys/:key_id/revoke when authenticated revokes existing key
# Failure/Error: let(:gpg_key) { create(:gpg_key, user: user) }
#
# TypeError:
# can't cast Hash
# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
# # ./spec/requests/api/users_spec.rb:908:in `block (4 levels) in <top (required)>'
# # ------------------
# # --- Caused by: ---
# # TypeError:
# # TypeError
# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
#
# This bug was fixed in Rails 5.1 by https://github.com/rails/rails/pull/24745/commits/aa062318c451512035c10898a1af95943b1a3803
if Gitlab.rails5?
ActiveSupport::Deprecation.warn("#{__FILE__} is a monkey patch which must be removed when upgrading to Rails 5.1")
if Rails.version.start_with?("5.1")
raise "Remove this monkey patch: #{__FILE__}"
end
# Copy-paste from https://github.com/kamipo/rails/blob/aa062318c451512035c10898a1af95943b1a3803/activerecord/lib/active_record/validations/uniqueness.rb
# including local fixes to make Rubocop happy again.
module ActiveRecord
module Validations
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
if record.persisted?
if finder_class.primary_key
relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
else
raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
end
end
relation = scope_relation(record, table, relation)
relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
error_options = options.except(:case_sensitive, :scope, :conditions)
error_options[:value] = value
record.errors.add(attribute, :taken, error_options)
end
rescue RangeError
end
protected
def build_relation(klass, table, attribute, value) #:nodoc:
if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
# the attribute may be an aliased attribute
if klass.attribute_alias?(attribute)
attribute = klass.attribute_alias(attribute)
end
attribute_name = attribute.to_s
column = klass.columns_hash[attribute_name]
cast_type = klass.type_for_attribute(attribute_name)
comparison =
if !options[:case_sensitive] && !value.nil?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
klass.connection.case_sensitive_comparison(table, attribute, column, value)
end
if value.nil?
klass.unscoped.where(comparison)
else
bind = Relation::QueryAttribute.new(attribute_name, value, cast_type)
klass.unscoped.where(comparison, bind)
end
end
end
end
end
end
......@@ -17,7 +17,11 @@ codequality:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- docker run
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
paths: [codeclimate.json]
```
......
......@@ -42,9 +42,9 @@ dast:
allow_failure: true
script:
- mkdir /zap/wrk/
- /zap/zap-baseline.py -J gl-dast-report.json -t $website \
--auth-url $login_url \
--auth-username "john.doe@example.com" \
- /zap/zap-baseline.py -J gl-dast-report.json -t $website
--auth-url $login_url
--auth-username "john.doe@example.com"
--auth-password "john-doe-password" || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
......
......@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
## Seeing the failure reason for jobs
> [Introduced][ce-5742] in GitLab 10.7.
> [Introduced][ce-17782] in GitLab 10.7.
When a pipeline fails or is allowed to fail, there are several places where you
can quickly check the reason it failed:
......@@ -88,6 +88,8 @@ In any case, if you hover over the failed job you can see the reason it failed.
![Pipeline detail](img/job_failure_reason.png)
From [GitLab 10.8][ce-17814] you can also see the reason it failed on the Job detail page.
## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11.
......@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly.
[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
[ce-17814]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
......@@ -2,10 +2,11 @@ module Gitlab
module Git
class Wiki
DuplicatePageError = Class.new(StandardError)
OperationError = Class.new(StandardError)
CommitDetails = Struct.new(:name, :email, :message) do
CommitDetails = Struct.new(:user_id, :username, :name, :email, :message) do
def to_h
{ name: name, email: email, message: message }
{ user_id: user_id, username: username, name: name, email: email, message: message }
end
end
PageBlob = Struct.new(:name)
......@@ -140,6 +141,10 @@ module Gitlab
end
end
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
private
# options:
......@@ -158,10 +163,6 @@ module Gitlab
offset: options[:offset])
end
def gollum_wiki
@gollum_wiki ||= Gollum::Wiki.new(@repository.path)
end
def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first
......@@ -201,12 +202,12 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
with_committer_with_hooks(commit_details) do |committer|
filename = File.basename(name)
dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
nil
gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
end
rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
......@@ -214,24 +215,23 @@ module Gitlab
def gollum_delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails)
gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
nil
with_committer_with_hooks(commit_details) do |committer|
gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
end
end
def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
with_committer_with_hooks(commit_details) do |committer|
page = gollum_page_by_path(page_path)
committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
# Instead of performing two renames if the title has changed,
# the update_page will only update the format and content and
# the rename_page will do anything related to moving/renaming
gollum_wiki.update_page(page, page.name, format, content, committer: committer)
gollum_wiki.rename_page(page, title, committer: committer)
committer.commit
nil
end
end
def gollum_find_page(title:, version: nil, dir: nil)
......@@ -288,6 +288,20 @@ module Gitlab
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
def committer_with_hooks(commit_details)
Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h)
end
def with_committer_with_hooks(commit_details, &block)
committer = committer_with_hooks(commit_details)
yield committer
committer.commit
nil
end
end
end
end
......@@ -200,6 +200,8 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
user_id: commit_details.user_id,
user_name: encode_binary(commit_details.username),
name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message)
......
......@@ -2,10 +2,14 @@ module Gitlab
module GlId
def self.gl_id(user)
if user.present?
"user-#{user.id}"
gl_id_from_id_value(user.id)
else
""
''
end
end
def self.gl_id_from_id_value(id)
"user-#{id}"
end
end
end
......@@ -20,6 +20,10 @@ module Gitlab
subject
end
def present(**attributes)
self
end
class_methods do
def presenter?
true
......
module Gitlab
module Wiki
class CommitterWithHooks < Gollum::Committer
attr_reader :gl_wiki
def initialize(gl_wiki, options = {})
@gl_wiki = gl_wiki
super(gl_wiki.gollum_wiki, options)
end
def commit
result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
@wiki.ref,
start_branch_name: @wiki.ref
) do |start_commit|
super(false)
end
result[:newrev]
rescue Gitlab::Git::HooksService::PreReceiveError => e
message = "Custom Hook failed: #{e.message}"
raise Gitlab::Git::Wiki::OperationError, message
end
private
def git_user
@git_user ||= Gitlab::Git::User.new(@options[:username],
@options[:name],
@options[:email],
gitlab_id)
end
def gitlab_id
Gitlab::GlId.gl_id_from_id_value(@options[:user_id])
end
end
end
end
require 'gitlab/styles/rubocop/model_helpers'
module RuboCop
module Cop
module Gitlab
class HasManyThroughScope < RuboCop::Cop::Cop
include ::Gitlab::Styles::Rubocop::ModelHelpers
MSG = 'Always provide an explicit scope calling auto_include(false) when using has_many :through'.freeze
def_node_search :through?, <<~PATTERN
(pair (sym :through) _)
PATTERN
def_node_matcher :has_many_through?, <<~PATTERN
(send nil? :has_many ... #through?)
PATTERN
def_node_search :disables_auto_include?, <<~PATTERN
(send _ :auto_include false)
PATTERN
def_node_matcher :scope_disables_auto_include?, <<~PATTERN
(block (send nil? :lambda) _ #disables_auto_include?)
PATTERN
def on_send(node)
return unless in_model?(node)
return unless has_many_through?(node)
target = node
scope_argument = node.children[3]
if scope_argument.children[0].children.last == :lambda
return if scope_disables_auto_include?(scope_argument)
target = scope_argument
end
add_offense(target, location: :expression)
end
end
end
end
end
# rubocop:disable Naming/FileName
require_relative 'cop/gitlab/has_many_through_scope'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
......
......@@ -190,7 +190,10 @@ describe Projects::JobsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to be_nil
end
it 'returns no job log message' do
expect(json_response['html']).to eq('No job log')
end
end
......
......@@ -243,5 +243,10 @@ FactoryBot.define do
failed
failure_reason 1
end
trait :api_failure do
failed
failure_reason 2
end
end
end
......@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do
end
context 'labels' do
it 'shows current labels when editing' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_requests
page.within('.value') do
expect(page).to have_selector('.label', count: 2)
expect(page).to have_content(development.title)
expect(page).to have_content(stretch.title)
end
end
end
it 'adds a single label' do
click_card(card)
......@@ -296,7 +312,9 @@ describe 'Issue Boards', :js do
wait_for_requests
within('.dropdown-menu-labels') do
click_link stretch.title
end
wait_for_requests
......
......@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do
......@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
issue.update_attributes(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
end
it 'shows the current set of labels' do
page.within('.issuable-show-labels') do
expect(page).to have_content label.title
end
end
it 'shows option to create a project label' do
page.within('.block.labels') do
expect(page).to have_content 'Create project'
......
......@@ -491,16 +491,18 @@ feature 'Jobs' do
end
end
describe "POST /:project/jobs/:id/retry" do
describe "POST /:project/jobs/:id/retry", :js do
context "Job from project", :js do
before do
job.run!
job.cancel!
visit project_job_path(project, job)
find('.js-cancel-job').click()
wait_for_requests
find('.js-retry-button').click
end
it 'shows the right status and buttons', :js do
it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
......
......@@ -44,6 +44,8 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
click_button 'Stage all'
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
......
......@@ -34,6 +34,8 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
click_button 'Stage all'
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
......
......@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project)
end
it 'does not include projects shared with the group' do
project = create(:project, namespace: group)
other_project = create(:project)
other_project.project_group_links.create(group: group,
group_access: ProjectGroupLink::MASTER)
expect(finder.execute).to contain_exactly(project)
end
context 'when archived is `true`' do
let(:params) { { archived: 'true' } }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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