Commit 97d0bfc1 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 5538-svg

* master: (151 commits)
  only return last_commit_sha in the JSON
  IDE sends last commit ID when committing changes
  Reuse viewer param and move logic to blob controller
  Removes unused imports and prettifies code
  Remove unexpected data store test suite
  Add ChatOps icon to docs
  Fix indentation for 'Current status' section
  Resolve docs conflicts
  Fix "[Rails5] '-1' is not a valid data_store"
  Changed the query string parameter to a string
  Remove scrollbar in Safari in repo settings page
  Resolve ""Click to expand" link in collapsed diffs should be blue"
  Fix missing dots from the CI job log output
  Fix template icons
  remove plural from 'label'
  Fix design artifact should not be removed
  Fixed linting error with trailing space in rb
  Fix node status badge alignment in mobile layout
  Fix node action buttons alignment
  Fix geo modal header
  ...
parents 3ff16995 0a7d50ac
...@@ -24,6 +24,8 @@ lib/gitlab/git/tag.rb ...@@ -24,6 +24,8 @@ lib/gitlab/git/tag.rb
ee/db/**/* ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/api/epics.rb
ee/lib/api/geo_nodes.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
......
...@@ -370,10 +370,10 @@ package-and-qa: ...@@ -370,10 +370,10 @@ package-and-qa:
<<: *single-script-job <<: *single-script-job
variables: variables:
<<: *single-script-job-variables <<: *single-script-job-variables
SCRIPT_NAME: trigger-build-omnibus SCRIPT_NAME: trigger-build
retry: 0 retry: 0
script: script:
- ./$SCRIPT_NAME - ./$SCRIPT_NAME omnibus
when: manual when: manual
only: only:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
......
...@@ -487,7 +487,7 @@ Style/EmptyLiteral: ...@@ -487,7 +487,7 @@ Style/EmptyLiteral:
- 'lib/gitlab/fogbugz_import/importer.rb' - 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/diff_collection.rb' - 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/gitaly_client.rb' - 'lib/gitlab/gitaly_client.rb'
- 'scripts/trigger-build-omnibus' - 'scripts/trigger-build'
- 'spec/features/merge_requests/versions_spec.rb' - 'spec/features/merge_requests/versions_spec.rb'
- 'spec/helpers/merge_requests_helper_spec.rb' - 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/lib/gitlab/request_context_spec.rb' - 'spec/lib/gitlab/request_context_spec.rb'
......
...@@ -302,19 +302,28 @@ For guidance on UX implementation at GitLab, please refer to our [Design System] ...@@ -302,19 +302,28 @@ For guidance on UX implementation at GitLab, please refer to our [Design System]
The UX team uses labels to manage their workflow. The UX team uses labels to manage their workflow.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention. The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook. To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux) of the handbook.
Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue. Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue.
The UX team has a special type label called ~"design artifact". This label indicates that the final output The UX team has a special type label called ~"design artifact". This label indicates that the final output
for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided. needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. ~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the To prevent the misunderstanding that a feature will be be delivered in the
existing issue or decide to create a whole new issue for the purpose of development. assigned milestone, when only UX design is planned for that milestone, the
Product Manager should create a separate issue for the ~"design artifact",
assign the ~UX, ~"design artifact" and ~"Deliverable" labels, add a milestone
and use a title that makes it clear that the scheduled issue is design only
(e.g. `Design exploration for XYZ`).
When the ~"design artifact" issue has been completed, the UXer removes the ~UX
label, adds the ~"UX ready" label and closes the issue. This indicates the
design artifact is complete. The UXer will also copy the designs to related
issues for implementation in an upcoming milestone.
## Issue tracker ## Issue tracker
......
...@@ -177,6 +177,7 @@ the stable branch are: ...@@ -177,6 +177,7 @@ the stable branch are:
* Fixes for [regressions](#regressions) * Fixes for [regressions](#regressions)
* Fixes for security issues * Fixes for security issues
* Fixes or improvements to automated QA scenarios * Fixes or improvements to automated QA scenarios
* Documentation updates for changes in the same release
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the During the feature freeze all merge requests that are meant to go into the
......
...@@ -119,7 +119,7 @@ const gfmRules = { ...@@ -119,7 +119,7 @@ const gfmRules = {
return el.outerHTML; return el.outerHTML;
}, },
'dl'(el, text) { 'dl'(el, text) {
let lines = text.trim().split('\n'); let lines = text.replace(/\n\n/g, '\n').trim().split('\n');
// Add two spaces to the front of subsequent list items lines, // Add two spaces to the front of subsequent list items lines,
// or leave the line entirely blank. // or leave the line entirely blank.
lines = lines.map((l) => { lines = lines.map((l) => {
...@@ -129,9 +129,13 @@ const gfmRules = { ...@@ -129,9 +129,13 @@ const gfmRules = {
return ` ${line}`; return ` ${line}`;
}); });
return `<dl>\n${lines.join('\n')}\n</dl>`; return `<dl>\n${lines.join('\n')}\n</dl>\n`;
}, },
'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) { 'dt, dd, summary, details'(el, text) {
const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>\n`;
},
'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
const tag = el.nodeName.toLowerCase(); const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>`; return `<${tag}>${text}</${tag}>`;
}, },
...@@ -215,22 +219,22 @@ const gfmRules = { ...@@ -215,22 +219,22 @@ const gfmRules = {
return text.replace(/^- /mg, '1. '); return text.replace(/^- /mg, '1. ');
}, },
'h1'(el, text) { 'h1'(el, text) {
return `# ${text.trim()}`; return `# ${text.trim()}\n`;
}, },
'h2'(el, text) { 'h2'(el, text) {
return `## ${text.trim()}`; return `## ${text.trim()}\n`;
}, },
'h3'(el, text) { 'h3'(el, text) {
return `### ${text.trim()}`; return `### ${text.trim()}\n`;
}, },
'h4'(el, text) { 'h4'(el, text) {
return `#### ${text.trim()}`; return `#### ${text.trim()}\n`;
}, },
'h5'(el, text) { 'h5'(el, text) {
return `##### ${text.trim()}`; return `##### ${text.trim()}\n`;
}, },
'h6'(el, text) { 'h6'(el, text) {
return `###### ${text.trim()}`; return `###### ${text.trim()}\n`;
}, },
'strong'(el, text) { 'strong'(el, text) {
return `**${text}**`; return `**${text}**`;
...@@ -241,11 +245,13 @@ const gfmRules = { ...@@ -241,11 +245,13 @@ const gfmRules = {
'del'(el, text) { 'del'(el, text) {
return `~~${text}~~`; return `~~${text}~~`;
}, },
'sup'(el, text) {
return `^${text}`;
},
'hr'(el) { 'hr'(el) {
return '-----'; // extra leading \n is to ensure that there is a blank line between
// a list followed by an hr, otherwise this breaks old redcarpet rendering
return '\n-----\n';
},
'p'(el, text) {
return `${text.trim()}\n`;
}, },
'table'(el) { 'table'(el) {
const theadEl = el.querySelector('thead'); const theadEl = el.querySelector('thead');
...@@ -263,7 +269,9 @@ const gfmRules = { ...@@ -263,7 +269,9 @@ const gfmRules = {
let before = ''; let before = '';
let after = ''; let after = '';
switch (cell.style.textAlign) { const alignment = cell.align || cell.style.textAlign;
switch (alignment) {
case 'center': case 'center':
before = ':'; before = ':';
after = ':'; after = ':';
......
...@@ -66,8 +66,14 @@ export default class CreateMergeRequestDropdown { ...@@ -66,8 +66,14 @@ export default class CreateMergeRequestDropdown {
} }
bindEvents() { bindEvents() {
this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this)); this.createMergeRequestButton.addEventListener(
this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this)); 'click',
this.onClickCreateMergeRequestButton.bind(this),
);
this.createTargetButton.addEventListener(
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this)); this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this)); this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this)); this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
...@@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown { ...@@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown {
checkAbilityToCreateBranch() { checkAbilityToCreateBranch() {
this.setUnavailableButtonState(); this.setUnavailableButtonState();
axios.get(this.canCreatePath) axios
.get(this.canCreatePath)
.then(({ data }) => { .then(({ data }) => {
this.setUnavailableButtonState(false); this.setUnavailableButtonState(false);
...@@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown { ...@@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown {
createBranch() { createBranch() {
this.isCreatingBranch = true; this.isCreatingBranch = true;
return axios.post(this.createBranchPath) return axios
.post(this.createBranchPath)
.then(({ data }) => { .then(({ data }) => {
this.branchCreated = true; this.branchCreated = true;
window.location.href = data.url; window.location.href = data.url;
...@@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown { ...@@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown {
createMergeRequest() { createMergeRequest() {
this.isCreatingMergeRequest = true; this.isCreatingMergeRequest = true;
return axios.post(this.createMrPath) return axios
.post(this.createMrPath)
.then(({ data }) => { .then(({ data }) => {
this.mergeRequestCreated = true; this.mergeRequestCreated = true;
window.location.href = data.url; window.location.href = data.url;
...@@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown { ...@@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') { getRef(ref, target = 'all') {
if (!ref) return false; if (!ref) return false;
return axios.get(this.refsPath + ref) return axios
.get(`${this.refsPath}${encodeURIComponent(ref)}`)
.then(({ data }) => { .then(({ data }) => {
const branches = data[Object.keys(data)[0]]; const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]]; const tags = data[Object.keys(data)[1]];
...@@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown { ...@@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown {
if (target === 'branch') { if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref); result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else { } else {
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || result =
CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true); CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result; this.suggestedRef = result;
} }
...@@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown { ...@@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown {
} }
isBusy() { isBusy() {
return this.isCreatingMergeRequest || return (
this.isCreatingMergeRequest ||
this.mergeRequestCreated || this.mergeRequestCreated ||
this.isCreatingBranch || this.isCreatingBranch ||
this.branchCreated || this.branchCreated ||
this.isGettingRef; this.isGettingRef
);
} }
onChangeInput(event) { onChangeInput(event) {
...@@ -271,7 +284,8 @@ export default class CreateMergeRequestDropdown { ...@@ -271,7 +284,8 @@ export default class CreateMergeRequestDropdown {
value = this.branchInput.value; value = this.branchInput.value;
} else if (event.target === this.refInput) { } else if (event.target === this.refInput) {
target = 'ref'; target = 'ref';
value = event.target.value.slice(0, event.target.selectionStart) + value =
event.target.value.slice(0, event.target.selectionStart) +
event.target.value.slice(event.target.selectionEnd); event.target.value.slice(event.target.selectionEnd);
} else { } else {
return false; return false;
...@@ -396,7 +410,8 @@ export default class CreateMergeRequestDropdown { ...@@ -396,7 +410,8 @@ export default class CreateMergeRequestDropdown {
showNotAvailableMessage(target) { showNotAvailableMessage(target) {
const { input, message } = this.getTargetData(target); const { input, message } = this.getTargetData(target);
const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available'); const text =
target === 'branch' ? __('Branch is already taken') : __('Source is not available');
this.removeMessage(target); this.removeMessage(target);
input.classList.add('gl-field-error-outline'); input.classList.add('gl-field-error-outline');
...@@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown { ...@@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown {
// target - 'branch' or 'ref' // target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref // ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) { updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`; const pathReplacement = `$1${encodeURIComponent(ref)}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath, this.createBranchPath = this.createBranchPath.replace(
pathReplacement); this.regexps[target].createBranchPath,
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath, pathReplacement,
pathReplacement); );
this.createMrPath = this.createMrPath.replace(
this.regexps[target].createMrPath,
pathReplacement,
);
} }
} }
...@@ -43,6 +43,15 @@ export default { ...@@ -43,6 +43,15 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
activeFileKey: {
type: String,
required: false,
default: null,
},
keyPrefix: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -113,8 +122,9 @@ export default { ...@@ -113,8 +122,9 @@ export default {
<list-item <list-item
:file="file" :file="file"
:action-component="itemActionComponent" :action-component="itemActionComponent"
:key-prefix="title" :key-prefix="keyPrefix"
:staged-list="stagedList" :staged-list="stagedList"
:active-file-key="activeFileKey"
/> />
</li> </li>
</ul> </ul>
......
...@@ -30,6 +30,11 @@ export default { ...@@ -30,6 +30,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
activeFileKey: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
iconName() { iconName() {
...@@ -39,6 +44,12 @@ export default { ...@@ -39,6 +44,12 @@ export default {
iconClass() { iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
}, },
fullKey() {
return `${this.keyPrefix}-${this.file.key}`;
},
isActive() {
return this.activeFileKey === this.fullKey;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -51,7 +62,7 @@ export default { ...@@ -51,7 +62,7 @@ export default {
openFileInEditor() { openFileInEditor() {
return this.openPendingTab({ return this.openPendingTab({
file: this.file, file: this.file,
keyPrefix: this.keyPrefix.toLowerCase(), keyPrefix: this.keyPrefix,
}).then(changeViewer => { }).then(changeViewer => {
if (changeViewer) { if (changeViewer) {
this.updateViewer(viewerTypes.diff); this.updateViewer(viewerTypes.diff);
...@@ -70,7 +81,12 @@ export default { ...@@ -70,7 +81,12 @@ export default {
</script> </script>
<template> <template>
<div class="multi-file-commit-list-item"> <div
:class="{
'is-active': isActive
}"
class="multi-file-commit-list-item"
>
<button <button
type="button" type="button"
class="multi-file-commit-list-path" class="multi-file-commit-list-path"
......
...@@ -94,7 +94,7 @@ export default { ...@@ -94,7 +94,7 @@ export default {
<p class="append-bottom-0"> <p class="append-bottom-0">
{{ __('Found errors in your .gitlab-ci.yml:') }} {{ __('Found errors in your .gitlab-ci.yml:') }}
</p> </p>
<p class="append-bottom-0"> <p class="append-bottom-0 break-word">
{{ latestPipeline.yamlError }} {{ latestPipeline.yamlError }}
</p> </p>
<p <p
......
...@@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; ...@@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue'; import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue'; import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants'; import * as consts from '../stores/modules/commit/constants';
import { activityBarViews } from '../constants'; import { activityBarViews, stageKeys } from '../constants';
export default { export default {
components: { components: {
...@@ -27,11 +27,14 @@ export default { ...@@ -27,11 +27,14 @@ export default {
'unusedSeal', 'unusedSeal',
]), ]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']), ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() { showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
}, },
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
},
}, },
watch: { watch: {
hasChanges() { hasChanges() {
...@@ -44,6 +47,7 @@ export default { ...@@ -44,6 +47,7 @@ export default {
if (this.lastOpenedFile) { if (this.lastOpenedFile) {
this.openPendingTab({ this.openPendingTab({
file: this.lastOpenedFile, file: this.lastOpenedFile,
keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
}) })
.then(changeViewer => { .then(changeViewer => {
if (changeViewer) { if (changeViewer) {
...@@ -62,6 +66,7 @@ export default { ...@@ -62,6 +66,7 @@ export default {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
}, },
}, },
stageKeys,
}; };
</script> </script>
...@@ -86,8 +91,10 @@ export default { ...@@ -86,8 +91,10 @@ export default {
> >
<commit-files-list <commit-files-list
:title="__('Unstaged')" :title="__('Unstaged')"
:key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles" :file-list="changedFiles"
:action-btn-text="__('Stage all')" :action-btn-text="__('Stage all')"
:active-file-key="activeFileKey"
class="is-first" class="is-first"
icon-name="unstaged" icon-name="unstaged"
action="stageAllChanges" action="stageAllChanges"
...@@ -95,9 +102,11 @@ export default { ...@@ -95,9 +102,11 @@ export default {
/> />
<commit-files-list <commit-files-list
:title="__('Staged')" :title="__('Staged')"
:key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles" :file-list="stagedFiles"
:action-btn-text="__('Unstage all')" :action-btn-text="__('Unstage all')"
:staged-list="true" :staged-list="true"
:active-file-key="activeFileKey"
icon-name="staged" icon-name="staged"
action="unstageAllChanges" action="unstageAllChanges"
item-action-component="unstage-button" item-action-component="unstage-button"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants'; import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import ExternalLink from './external_link.vue'; import ExternalLink from './external_link.vue';
...@@ -9,6 +10,7 @@ import ExternalLink from './external_link.vue'; ...@@ -9,6 +10,7 @@ import ExternalLink from './external_link.vue';
export default { export default {
components: { components: {
ContentViewer, ContentViewer,
DiffViewer,
ExternalLink, ExternalLink,
}, },
props: { props: {
...@@ -18,7 +20,13 @@ export default { ...@@ -18,7 +20,13 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']), ...mapState([
'rightPanelCollapsed',
'viewer',
'panelResizing',
'currentActivityView',
'rightPane',
]),
...mapGetters([ ...mapGetters([
'currentMergeRequest', 'currentMergeRequest',
'getStagedFile', 'getStagedFile',
...@@ -29,9 +37,18 @@ export default { ...@@ -29,9 +37,18 @@ export default {
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
showContentViewer() {
return (
(this.shouldHideEditor || this.file.viewMode === 'preview') &&
(this.viewer !== viewerTypes.mr || !this.file.mrChange)
);
},
showDiffViewer() {
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
},
editTabCSS() { editTabCSS() {
return { return {
active: this.file.viewMode === 'edit', active: this.file.viewMode === 'editor',
}; };
}, },
previewTabCSS() { previewTabCSS() {
...@@ -53,7 +70,7 @@ export default { ...@@ -53,7 +70,7 @@ export default {
if (this.currentActivityView !== activityBarViews.edit) { if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({ this.setFileViewMode({
file: this.file, file: this.file,
viewMode: 'edit', viewMode: 'editor',
}); });
} }
} }
...@@ -62,7 +79,7 @@ export default { ...@@ -62,7 +79,7 @@ export default {
if (this.currentActivityView !== activityBarViews.edit) { if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({ this.setFileViewMode({
file: this.file, file: this.file,
viewMode: 'edit', viewMode: 'editor',
}); });
} }
}, },
...@@ -77,6 +94,9 @@ export default { ...@@ -77,6 +94,9 @@ export default {
this.editor.updateDimensions(); this.editor.updateDimensions();
} }
}, },
rightPane() {
this.editor.updateDimensions();
},
}, },
beforeDestroy() { beforeDestroy() {
this.editor.dispose(); this.editor.dispose();
...@@ -197,7 +217,7 @@ export default { ...@@ -197,7 +217,7 @@ export default {
<a <a
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> @click.prevent="setFileViewMode({ file, viewMode: 'editor' })">
<template v-if="viewer === $options.viewerTypes.edit"> <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }} {{ __('Edit') }}
</template> </template>
...@@ -222,7 +242,7 @@ export default { ...@@ -222,7 +242,7 @@ export default {
/> />
</div> </div>
<div <div
v-show="!shouldHideEditor && file.viewMode === 'edit'" v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor" ref="editor"
:class="{ :class="{
'is-readonly': isCommitModeActive, 'is-readonly': isCommitModeActive,
...@@ -231,10 +251,18 @@ export default { ...@@ -231,10 +251,18 @@ export default {
> >
</div> </div>
<content-viewer <content-viewer
v-if="shouldHideEditor || file.viewMode === 'preview'" v-if="showContentViewer"
:content="file.content || file.raw" :content="file.content || file.raw"
:path="file.rawPath || file.path" :path="file.rawPath || file.path"
:file-size="file.size" :file-size="file.size"
:project-path="file.projectId"/> :project-path="file.projectId"/>
<diff-viewer
v-if="showDiffViewer"
:diff-mode="file.mrChange.diffMode"
:new-path="file.mrChange.new_path"
:new-sha="currentMergeRequest.sha"
:old-path="file.mrChange.old_path"
:old-sha="currentMergeRequest.baseCommitSha"
:project-path="file.projectId"/>
</div> </div>
</template> </template>
...@@ -21,7 +21,19 @@ export const viewerTypes = { ...@@ -21,7 +21,19 @@ export const viewerTypes = {
diff: 'diff', diff: 'diff',
}; };
export const diffModes = {
replaced: 'replaced',
new: 'new',
deleted: 'deleted',
renamed: 'renamed',
};
export const rightSidebarViews = { export const rightSidebarViews = {
pipelines: 'pipelines-list', pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail', jobsDetail: 'jobs-detail',
}; };
export const stageKeys = {
unstaged: 'unstaged',
staged: 'staged',
};
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
return Vue.http.get(endpoint, { params: { format: 'json' } }); return Vue.http.get(endpoint, { params: { format: 'json' } });
}, },
getFileData(endpoint) { getFileData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } }); return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } });
}, },
getRawFileData(file) { getRawFileData(file) {
if (file.tempFile) { if (file.tempFile) {
......
...@@ -49,31 +49,6 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { ...@@ -49,31 +49,6 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
}; };
export const checkCommitStatus = ({ rootState }) =>
service
.getBranchData(rootState.currentProjectId, rootState.currentBranchId)
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
}
return false;
})
.catch(() =>
flash(
__('Error checking branch data. Please try again.'),
'alert',
document,
null,
false,
true,
),
);
export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => { export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
const selectedProject = rootState.projects[rootState.currentProjectId]; const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = { const lastCommit = {
...@@ -128,24 +103,17 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data } ...@@ -128,24 +103,17 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); const payload = createCommitPayload({
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); branch: getters.branchName,
newBranch,
state,
rootState,
});
commit(types.UPDATE_LOADING, true); commit(types.UPDATE_LOADING, true);
return getCommitStatus return service
.then( .commit(rootState.currentProjectId, payload)
branchChanged =>
new Promise(resolve => {
if (branchChanged) {
// show the modal with a Bootstrap call
$('#ide-create-branch-modal').modal('show');
} else {
resolve();
}
}),
)
.then(() => service.commit(rootState.currentProjectId, payload))
.then(({ data }) => { .then(({ data }) => {
commit(types.UPDATE_LOADING, false); commit(types.UPDATE_LOADING, false);
...@@ -220,12 +188,16 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -220,12 +188,16 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
); );
}) })
.catch(err => { .catch(err => {
let errMsg = __('Error committing changes. Please try again.'); if (err.response.status === 400) {
if (err.response.data && err.response.data.message) { $('#ide-create-branch-modal').modal('show');
errMsg += ` (${stripHtml(err.response.data.message)})`; } else {
let errMsg = __('Error committing changes. Please try again.');
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
} }
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
commit(types.UPDATE_LOADING, false); commit(types.UPDATE_LOADING, false);
}); });
......
...@@ -31,15 +31,16 @@ export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => { ...@@ -31,15 +31,16 @@ export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
commit(rootTypes.CLEAR_PROJECTS, null, { root: true }); commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true }); commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
commit(rootTypes.RESET_OPEN_FILES, null, { root: true }); commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
dispatch('pipelines/resetLatestPipeline', null, { root: true });
dispatch('setCurrentBranchId', '', { root: true }); dispatch('setCurrentBranchId', '', { root: true });
dispatch('pipelines/stopPipelinePolling', null, { root: true }) dispatch('pipelines/stopPipelinePolling', null, { root: true })
.then(() => { .then(() => {
dispatch('pipelines/resetLatestPipeline', null, { root: true });
dispatch('pipelines/clearEtagPoll', null, { root: true }); dispatch('pipelines/clearEtagPoll', null, { root: true });
}) })
.catch(e => { .catch(e => {
throw e; throw e;
}); });
dispatch('setRightPane', null, { root: true });
router.push(`/project/${projectPath}/merge_requests/${id}`); router.push(`/project/${projectPath}/merge_requests/${id}`);
}; };
......
...@@ -106,7 +106,9 @@ export const fetchJobTrace = ({ dispatch, state }) => { ...@@ -106,7 +106,9 @@ export const fetchJobTrace = ({ dispatch, state }) => {
.catch(() => dispatch('receiveJobTraceError')); .catch(() => dispatch('receiveJobTraceError'));
}; };
export const resetLatestPipeline = ({ commit }) => export const resetLatestPipeline = ({ commit }) => {
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
commit(types.SET_DETAIL_JOB, null);
};
export default () => {}; export default () => {};
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { diffModes } from '../../constants';
export default { export default {
[types.SET_FILE_ACTIVE](state, { path, active }) { [types.SET_FILE_ACTIVE](state, { path, active }) {
...@@ -46,6 +47,7 @@ export default { ...@@ -46,6 +47,7 @@ export default {
baseRaw: null, baseRaw: null,
html: data.html, html: data.html,
size: data.size, size: data.size,
lastCommitSha: data.last_commit_sha,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
...@@ -85,8 +87,19 @@ export default { ...@@ -85,8 +87,19 @@ export default {
}); });
}, },
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) { [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
let diffMode = diffModes.replaced;
if (mrChange.new_file) {
diffMode = diffModes.new;
} else if (mrChange.deleted_file) {
diffMode = diffModes.deleted;
} else if (mrChange.renamed_file) {
diffMode = diffModes.renamed;
}
Object.assign(state.entries[file.path], { Object.assign(state.entries[file.path], {
mrChange, mrChange: {
...mrChange,
diffMode,
},
}); });
}, },
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) { [types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
......
...@@ -17,6 +17,7 @@ export const dataStructure = () => ({ ...@@ -17,6 +17,7 @@ export const dataStructure = () => ({
changed: false, changed: false,
staged: false, staged: false,
lastCommitPath: '', lastCommitPath: '',
lastCommitSha: '',
lastCommit: { lastCommit: {
id: '', id: '',
url: '', url: '',
...@@ -39,7 +40,7 @@ export const dataStructure = () => ({ ...@@ -39,7 +40,7 @@ export const dataStructure = () => ({
editorColumn: 1, editorColumn: 1,
fileLanguage: '', fileLanguage: '',
eol: '', eol: '',
viewMode: 'edit', viewMode: 'editor',
previewMode: null, previewMode: null,
size: 0, size: 0,
parentPath: null, parentPath: null,
...@@ -104,7 +105,7 @@ export const setPageTitle = title => { ...@@ -104,7 +105,7 @@ export const setPageTitle = title => {
document.title = title; document.title = title;
}; };
export const createCommitPayload = (branch, newBranch, state, rootState) => ({ export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({
branch, branch,
commit_message: state.commitMessage, commit_message: state.commitMessage,
actions: rootState.stagedFiles.map(f => ({ actions: rootState.stagedFiles.map(f => ({
...@@ -112,6 +113,7 @@ export const createCommitPayload = (branch, newBranch, state, rootState) => ({ ...@@ -112,6 +113,7 @@ export const createCommitPayload = (branch, newBranch, state, rootState) => ({
file_path: f.path, file_path: f.path,
content: f.content, content: f.content,
encoding: f.base64 ? 'base64' : 'text', encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch ? undefined : f.lastCommitSha,
})), })),
start_branch: newBranch ? rootState.currentBranchId : undefined, start_branch: newBranch ? rootState.currentBranchId : undefined,
}); });
......
/* eslint-disable no-param-reassign, comma-dangle */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';
import utilsMixin from '../mixins/line_conflict_utils';
((global) => { (global => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({ global.mergeConflicts.inlineConflictLines = Vue.extend({
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], mixins: [utilsMixin, actionsMixin],
props: { props: {
file: { file: {
type: Object, type: Object,
......
/* eslint-disable no-param-reassign, comma-dangle */ /* eslint-disable no-param-reassign, comma-dangle */
import Vue from 'vue'; import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';
import utilsMixin from '../mixins/line_conflict_utils';
((global) => { ((global) => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({ global.mergeConflicts.parallelConflictLines = Vue.extend({
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], mixins: [utilsMixin, actionsMixin],
props: { props: {
file: { file: {
type: Object, type: Object,
......
/* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
((global) => { export default class MergeConflictsService {
global.mergeConflicts = global.mergeConflicts || {}; constructor(options) {
this.conflictsPath = options.conflictsPath;
class mergeConflictsService { this.resolveConflictsPath = options.resolveConflictsPath;
constructor(options) { }
this.conflictsPath = options.conflictsPath;
this.resolveConflictsPath = options.resolveConflictsPath;
}
fetchConflictsData() {
return axios.get(this.conflictsPath);
}
submitResolveConflicts(data) { fetchConflictsData() {
return axios.post(this.resolveConflictsPath, data); return axios.get(this.conflictsPath);
}
} }
global.mergeConflicts.mergeConflictsService = mergeConflictsService; submitResolveConflicts(data) {
})(window.gl || (window.gl = {})); return axios.post(this.resolveConflictsPath, data);
}
}
/* eslint-disable new-cap, comma-dangle, no-new */
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../flash'; import createFlash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar'; import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store'; import './merge_conflict_store';
import './merge_conflict_service'; import MergeConflictsService from './merge_conflict_service';
import './mixins/line_conflict_utils';
import './mixins/line_conflict_actions';
import './components/diff_file_editor'; import './components/diff_file_editor';
import './components/inline_conflict_lines'; import './components/inline_conflict_lines';
import './components/parallel_conflict_lines'; import './components/parallel_conflict_lines';
...@@ -17,9 +13,9 @@ export default function initMergeConflicts() { ...@@ -17,9 +13,9 @@ export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive'; const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts'); const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({ const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath, conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
}); });
initIssuableSidebar(); initIssuableSidebar();
...@@ -29,17 +25,26 @@ export default function initMergeConflicts() { ...@@ -29,17 +25,26 @@ export default function initMergeConflicts() {
components: { components: {
'diff-file-editor': gl.mergeConflicts.diffFileEditor, 'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines,
}, },
data: mergeConflictsStore.state, data: mergeConflictsStore.state,
computed: { computed: {
conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, conflictsCountText() {
readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, return mergeConflictsStore.getConflictsCountText();
commitButtonText() { return mergeConflictsStore.getCommitButtonText(); }, },
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } readyToCommit() {
return mergeConflictsStore.isReadyToCommit();
},
commitButtonText() {
return mergeConflictsStore.getCommitButtonText();
},
showDiffViewTypeSwitcher() {
return mergeConflictsStore.fileTextTypePresent();
},
}, },
created() { created() {
mergeConflictsService.fetchConflictsData() mergeConflictsService
.fetchConflictsData()
.then(({ data }) => { .then(({ data }) => {
if (data.type === 'error') { if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message); mergeConflictsStore.setFailedRequest(data.message);
...@@ -87,9 +92,9 @@ export default function initMergeConflicts() { ...@@ -87,9 +92,9 @@ export default function initMergeConflicts() {
}) })
.catch(() => { .catch(() => {
mergeConflictsStore.setSubmitState(false); mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!'); createFlash('Failed to save merge conflicts resolutions. Please try again!');
}); });
} },
} },
}); });
} }
/* eslint-disable no-param-reassign, comma-dangle */ export default {
methods: {
((global) => { handleSelected(file, sectionId, selection) {
global.mergeConflicts = global.mergeConflicts || {}; gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
},
global.mergeConflicts.actions = { },
methods: { };
handleSelected(file, sectionId, selection) {
gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
}
}
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign, quote-props, comma-dangle */ export default {
methods: {
((global) => { lineCssClass(line) {
global.mergeConflicts = global.mergeConflicts || {}; return {
head: line.isHead,
global.mergeConflicts.utils = { origin: line.isOrigin,
methods: { match: line.hasMatch,
lineCssClass(line) { selected: line.isSelected,
return { unselected: line.isUnselected,
'head': line.isHead, };
'origin': line.isOrigin, },
'match': line.hasMatch, },
'selected': line.isSelected, };
'unselected': line.isUnselected
};
}
}
};
})(window.gl || (window.gl = {}));
...@@ -362,7 +362,7 @@ export default class MergeRequestTabs { ...@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
// //
// status - Boolean, true to show, false to hide // status - Boolean, true to show, false to hide
toggleLoading(status) { toggleLoading(status) {
$('.mr-loading-status .loading').toggleClass('hidden', !status); $('.mr-loading-status .loading').toggleClass('hide', !status);
} }
diffViewType() { diffViewType() {
......
...@@ -56,7 +56,7 @@ export default class MilestoneSelect { ...@@ -56,7 +56,7 @@ export default class MilestoneSelect {
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template( milestoneLinkTemplate = _.template(
'<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
); );
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
} }
......
...@@ -1675,7 +1675,7 @@ export default class Notes { ...@@ -1675,7 +1675,7 @@ export default class Notes {
<div class="note-header"> <div class="note-header">
<div class="note-header-info"> <div class="note-header-info">
<a href="/${_.escape(currentUsername)}"> <a href="/${_.escape(currentUsername)}">
<span class="d-none d-sm-block">${_.escape( <span class="d-none d-sm-inline-block">${_.escape(
currentUsername, currentUsername,
)}</span> )}</span>
<span class="note-headline-light">${_.escape( <span class="note-headline-light">${_.escape(
...@@ -1694,7 +1694,7 @@ export default class Notes { ...@@ -1694,7 +1694,7 @@ export default class Notes {
</li>`, </li>`,
); );
$tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname)); $tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
$tempNote $tempNote
.find('.note-headline-light') .find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`); .text(`@${_.escape(currentUsername)}`);
......
...@@ -62,13 +62,13 @@ export default class UsernameValidator { ...@@ -62,13 +62,13 @@ export default class UsernameValidator {
return this.setPendingState(); return this.setPendingState();
} }
if (!this.state.available) {
return this.setUnavailableState();
}
if (!this.state.valid) { if (!this.state.valid) {
return this.setInvalidState(); return this.setInvalidState();
} }
if (!this.state.available) {
return this.setUnavailableState();
}
} }
interceptInvalid(event) { interceptInvalid(event) {
...@@ -89,7 +89,6 @@ export default class UsernameValidator { ...@@ -89,7 +89,6 @@ export default class UsernameValidator {
setAvailabilityState(usernameTaken) { setAvailabilityState(usernameTaken) {
if (usernameTaken) { if (usernameTaken) {
this.state.valid = false;
this.state.available = false; this.state.available = false;
} else { } else {
this.state.available = true; this.state.available = true;
......
...@@ -180,7 +180,7 @@ export default class UserTabs { ...@@ -180,7 +180,7 @@ export default class UserTabs {
} }
toggleLoading(status) { toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status); return this.$parentEl.find('.loading-status .loading').toggleClass('hide', !status);
} }
setCurrentAction(source) { setCurrentAction(source) {
......
...@@ -56,7 +56,7 @@ export default { ...@@ -56,7 +56,7 @@ export default {
<gl-modal <gl-modal
:id="`modal-peek-${metric}-details`" :id="`modal-peek-${metric}-details`"
:header-title-text="header" :header-title-text="header"
modal-size="lg" modal-size="xl"
class="performance-bar-modal" class="performance-bar-modal"
> >
<table <table
...@@ -71,7 +71,7 @@ export default { ...@@ -71,7 +71,7 @@ export default {
<td <td
v-for="key in keys" v-for="key in keys"
:key="key" :key="key"
class="break-word all-words" class="break-word"
> >
{{ item[key] }} {{ item[key] }}
</td> </td>
......
...@@ -90,7 +90,7 @@ const bindEvents = () => { ...@@ -90,7 +90,7 @@ const bindEvents = () => {
function chooseTemplate() { function chooseTemplate() {
$('.template-option').hide(); $('.template-option').hide();
$projectFieldsForm.addClass('selected'); $projectFieldsForm.addClass('selected');
$selectedIcon.removeClass('active'); $selectedIcon.removeClass('d-block');
const value = $(this).val(); const value = $(this).val();
const templates = { const templates = {
rails: { rails: {
...@@ -109,7 +109,7 @@ const bindEvents = () => { ...@@ -109,7 +109,7 @@ const bindEvents = () => {
const selectedTemplate = templates[value]; const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text); $selectedTemplateText.text(selectedTemplate.text);
$(selectedTemplate.icon).addClass('active'); $(selectedTemplate.icon).addClass('d-block');
$templateProjectNameInput.focus(); $templateProjectNameInput.focus();
} }
......
...@@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight'; ...@@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>'; const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>';
export default class SingleFileDiff { export default class SingleFileDiff {
constructor(file) { constructor(file) {
......
...@@ -32,7 +32,10 @@ export default { ...@@ -32,7 +32,10 @@ export default {
<div class="file-container"> <div class="file-container">
<div class="file-content"> <div class="file-content">
<p class="prepend-top-10 file-info"> <p class="prepend-top-10 file-info">
{{ fileName }} ({{ fileSizeReadable }}) {{ fileName }}
<template v-if="fileSize > 0">
({{ fileSizeReadable }})
</template>
</p> </p>
<a <a
:href="path" :href="path"
......
<script> <script>
import _ from 'underscore';
import { numberToHumanSize } from '../../../../lib/utils/number_utils'; import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default { export default {
...@@ -12,6 +13,10 @@ export default { ...@@ -12,6 +13,10 @@ export default {
required: false, required: false,
default: 0, default: 0,
}, },
renderInfo: {
type: Boolean,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -26,14 +31,34 @@ export default { ...@@ -26,14 +31,34 @@ export default {
return numberToHumanSize(this.fileSize); return numberToHumanSize(this.fileSize);
}, },
}, },
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
// The onImgLoad may have happened before the control was actually mounted
this.onImgLoad();
this.resizeThrottled = _.throttle(this.onImgLoad, 400);
window.addEventListener('resize', this.resizeThrottled, false);
},
methods: { methods: {
onImgLoad() { onImgLoad() {
const contentImg = this.$refs.contentImg; const contentImg = this.$refs.contentImg;
this.isZoomable =
contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
this.width = contentImg.naturalWidth; if (contentImg) {
this.height = contentImg.naturalHeight; this.isZoomable =
contentImg.naturalWidth > contentImg.width ||
contentImg.naturalHeight > contentImg.height;
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
this.$emit('imgLoaded', {
width: this.width,
height: this.height,
renderedWidth: contentImg.clientWidth,
renderedHeight: contentImg.clientHeight,
});
}
}, },
onImgClick() { onImgClick() {
if (this.isZoomable) this.isZoomed = !this.isZoomed; if (this.isZoomable) this.isZoomed = !this.isZoomed;
...@@ -47,20 +72,22 @@ export default { ...@@ -47,20 +72,22 @@ export default {
<div class="file-content image_file"> <div class="file-content image_file">
<img <img
ref="contentImg" ref="contentImg"
:class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }" :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path" :src="path"
:alt="path" :alt="path"
@load="onImgLoad" @load="onImgLoad"
@click="onImgClick"/> @click="onImgClick"/>
<p class="file-info prepend-top-10"> <p
v-if="renderInfo"
class="file-info prepend-top-10">
<template v-if="fileSize>0"> <template v-if="fileSize>0">
{{ fileSizeReadable }} {{ fileSizeReadable }}
</template> </template>
<template v-if="fileSize>0 && width && height"> <template v-if="fileSize>0 && width && height">
- |
</template> </template>
<template v-if="width && height"> <template v-if="width && height">
{{ width }} x {{ height }} W: {{ width }} | H: {{ height }}
</template> </template>
</p> </p>
</div> </div>
......
export const diffModes = {
replaced: 'replaced',
new: 'new',
deleted: 'deleted',
renamed: 'renamed',
};
export const imageViewMode = {
twoup: 'twoup',
swipe: 'swipe',
onion: 'onion',
};
<script>
import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
export default {
props: {
diffMode: {
type: String,
required: true,
},
newPath: {
type: String,
required: true,
},
newSha: {
type: String,
required: true,
},
oldPath: {
type: String,
required: true,
},
oldSha: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
computed: {
viewer() {
if (!this.newPath) return null;
const previewInfo = viewerInformationForPath(this.newPath);
if (!previewInfo) return DownloadDiffViewer;
switch (previewInfo.id) {
case 'image':
return ImageDiffViewer;
default:
return DownloadDiffViewer;
}
},
fullOldPath() {
return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
},
fullNewPath() {
return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
},
},
};
</script>
<template>
<div
v-if="viewer"
class="diff-file preview-container">
<component
:is="viewer"
:diff-mode="diffMode"
:new-path="fullNewPath"
:old-path="fullOldPath"
:project-path="projectPath"
/>
</div>
</template>
<script>
import DownloadViewer from '../../content_viewer/viewers/download_viewer.vue';
import { diffModes } from '../constants';
export default {
components: {
DownloadViewer,
},
props: {
diffMode: {
type: String,
required: true,
},
newPath: {
type: String,
required: true,
},
oldPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
diffModes,
};
</script>
<template>
<div class="diff-file-container">
<div class="diff-viewer">
<div
v-if="diffMode === $options.diffModes.replaced"
class="two-up view row">
<div class="col-sm-6 deleted">
<download-viewer
:path="oldPath"
:project-path="projectPath"
/>
</div>
<div class="col-sm-6 added">
<download-viewer
:path="newPath"
:project-path="projectPath"
/>
</div>
</div>
<div
v-else-if="diffMode === $options.diffModes.new"
class="added">
<download-viewer
:path="newPath"
:project-path="projectPath"
/>
</div>
<div
v-else
class="deleted">
<download-viewer
:path="oldPath"
:project-path="projectPath"
/>
</div>
</div>
</div>
</template>
<script>
import { pixeliseValue } from '../../../lib/utils/dom_utils';
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
export default {
components: {
ImageViewer,
},
props: {
newPath: {
type: String,
required: true,
},
oldPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
onionMaxWidth: undefined,
onionMaxHeight: undefined,
onionOldImgInfo: null,
onionNewImgInfo: null,
onionDraggerPos: 0,
onionOpacity: 1,
dragging: false,
};
},
computed: {
onionMaxPixelWidth() {
return pixeliseValue(this.onionMaxWidth);
},
onionMaxPixelHeight() {
return pixeliseValue(this.onionMaxHeight);
},
onionDraggerPixelPos() {
return pixeliseValue(this.onionDraggerPos);
},
},
beforeDestroy() {
document.body.removeEventListener('mouseup', this.stopDrag);
this.$refs.dragger.removeEventListener('mousedown', this.startDrag);
},
methods: {
dragMove(e) {
if (!this.dragging) return;
const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left;
const dragTrackWidth =
this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
let leftValue = left;
if (leftValue < 0) leftValue = 0;
if (leftValue > dragTrackWidth) leftValue = dragTrackWidth;
this.onionOpacity = left / dragTrackWidth;
this.onionDraggerPos = leftValue;
},
startDrag() {
this.dragging = true;
document.body.style.userSelect = 'none';
document.body.addEventListener('mousemove', this.dragMove);
},
stopDrag() {
this.dragging = false;
document.body.style.userSelect = '';
document.body.removeEventListener('mousemove', this.dragMove);
},
prepareOnionSkin() {
if (this.onionOldImgInfo && this.onionNewImgInfo) {
this.onionMaxWidth = Math.max(
this.onionOldImgInfo.renderedWidth,
this.onionNewImgInfo.renderedWidth,
);
this.onionMaxHeight = Math.max(
this.onionOldImgInfo.renderedHeight,
this.onionNewImgInfo.renderedHeight,
);
this.onionOpacity = 1;
this.onionDraggerPos =
this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
document.body.addEventListener('mouseup', this.stopDrag);
}
},
onionNewImgLoaded(imgInfo) {
this.onionNewImgInfo = imgInfo;
this.prepareOnionSkin();
},
onionOldImgLoaded(imgInfo) {
this.onionOldImgInfo = imgInfo;
this.prepareOnionSkin();
},
},
};
</script>
<template>
<div class="onion-skin view">
<div
:style="{
'width': onionMaxPixelWidth,
'height': onionMaxPixelHeight,
'user-select': dragging === true ? 'none' : '',
}"
class="onion-skin-frame">
<div
:style="{
'width': onionMaxPixelWidth,
'height': onionMaxPixelHeight,
}"
class="frame deleted">
<image-viewer
key="onionOldImg"
:render-info="false"
:path="oldPath"
:project-path="projectPath"
@imgLoaded="onionOldImgLoaded"
/>
</div>
<div
ref="addedFrame"
:style="{
'opacity': onionOpacity,
'width': onionMaxPixelWidth,
'height': onionMaxPixelHeight,
}"
class="added frame">
<image-viewer
key="onionNewImg"
:render-info="false"
:path="newPath"
:project-path="projectPath"
@imgLoaded="onionNewImgLoaded"
/>
</div>
<div class="controls">
<div class="transparent"></div>
<div
ref="dragTrack"
class="drag-track"
@mousedown="startDrag"
@mouseup="stopDrag">
<div
ref="dragger"
:style="{ 'left': onionDraggerPixelPos }"
class="dragger">
</div>
</div>
<div class="opaque"></div>
</div>
</div>
</div>
</template>
<script>
import _ from 'underscore';
import { pixeliseValue } from '../../../lib/utils/dom_utils';
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
export default {
components: {
ImageViewer,
},
props: {
newPath: {
type: String,
required: true,
},
oldPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
dragging: false,
swipeOldImgInfo: null,
swipeNewImgInfo: null,
swipeMaxWidth: undefined,
swipeMaxHeight: undefined,
swipeBarPos: 1,
swipeWrapWidth: undefined,
};
},
computed: {
swipeMaxPixelWidth() {
return pixeliseValue(this.swipeMaxWidth);
},
swipeMaxPixelHeight() {
return pixeliseValue(this.swipeMaxHeight);
},
swipeWrapPixelWidth() {
return pixeliseValue(this.swipeWrapWidth);
},
swipeBarPixelPos() {
return pixeliseValue(this.swipeBarPos);
},
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
document.body.removeEventListener('mouseup', this.stopDrag);
document.body.removeEventListener('mousemove', this.dragMove);
},
mounted() {
window.addEventListener('resize', this.resize, false);
},
methods: {
dragMove(e) {
if (!this.dragging) return;
let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left;
const spaceLeft = 20;
const { clientWidth } = this.$refs.swipeFrame;
if (leftValue <= 0) {
leftValue = 0;
} else if (leftValue > clientWidth - spaceLeft) {
leftValue = clientWidth - spaceLeft;
}
this.swipeWrapWidth = this.swipeMaxWidth - leftValue;
this.swipeBarPos = leftValue;
},
startDrag() {
this.dragging = true;
document.body.style.userSelect = 'none';
document.body.addEventListener('mousemove', this.dragMove);
},
stopDrag() {
this.dragging = false;
document.body.style.userSelect = '';
document.body.removeEventListener('mousemove', this.dragMove);
},
prepareSwipe() {
if (this.swipeOldImgInfo && this.swipeNewImgInfo) {
// Add 2 for border width
this.swipeMaxWidth =
Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2;
this.swipeWrapWidth = this.swipeMaxWidth;
this.swipeMaxHeight =
Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2;
document.body.addEventListener('mouseup', this.stopDrag);
}
},
swipeNewImgLoaded(imgInfo) {
this.swipeNewImgInfo = imgInfo;
this.prepareSwipe();
},
swipeOldImgLoaded(imgInfo) {
this.swipeOldImgInfo = imgInfo;
this.prepareSwipe();
},
resize: _.throttle(function throttledResize() {
this.swipeBarPos = 0;
}, 400),
},
};
</script>
<template>
<div class="swipe view">
<div
ref="swipeFrame"
:style="{
'width': swipeMaxPixelWidth,
'height': swipeMaxPixelHeight,
}"
class="swipe-frame">
<div class="frame deleted">
<image-viewer
key="swipeOldImg"
ref="swipeOldImg"
:render-info="false"
:path="oldPath"
:project-path="projectPath"
@imgLoaded="swipeOldImgLoaded"
/>
</div>
<div
ref="swipeWrap"
:style="{
'width': swipeWrapPixelWidth,
'height': swipeMaxPixelHeight,
}"
class="swipe-wrap">
<div class="frame added">
<image-viewer
key="swipeNewImg"
:render-info="false"
:path="newPath"
:project-path="projectPath"
@imgLoaded="swipeNewImgLoaded"
/>
</div>
</div>
<span
ref="swipeBar"
:style="{ 'left': swipeBarPixelPos }"
class="swipe-bar"
@mousedown="startDrag"
@mouseup="stopDrag">
<span class="top-handle"></span>
<span class="bottom-handle"></span>
</span>
</div>
</div>
</template>
<script>
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
export default {
components: {
ImageViewer,
},
props: {
newPath: {
type: String,
required: true,
},
oldPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div class="two-up view row">
<div class="col-sm-6 frame deleted">
<image-viewer
:path="oldPath"
:project-path="projectPath"
/>
</div>
<div class="col-sm-6 frame added">
<image-viewer
:path="newPath"
:project-path="projectPath"
/>
</div>
</div>
</template>
<script>
import ImageViewer from '../../content_viewer/viewers/image_viewer.vue';
import TwoUpViewer from './image_diff/two_up_viewer.vue';
import SwipeViewer from './image_diff/swipe_viewer.vue';
import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
import { diffModes, imageViewMode } from '../constants';
export default {
components: {
ImageViewer,
TwoUpViewer,
SwipeViewer,
OnionSkinViewer,
},
props: {
diffMode: {
type: String,
required: true,
},
newPath: {
type: String,
required: true,
},
oldPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
mode: imageViewMode.twoup,
};
},
methods: {
changeMode(newMode) {
this.mode = newMode;
},
},
diffModes,
imageViewMode,
};
</script>
<template>
<div class="diff-file-container">
<div
v-if="diffMode === $options.diffModes.replaced"
class="diff-viewer">
<div class="image js-replaced-image">
<two-up-viewer
v-if="mode === $options.imageViewMode.twoup"
v-bind="$props"/>
<swipe-viewer
v-else-if="mode === $options.imageViewMode.swipe"
v-bind="$props"/>
<onion-skin-viewer
v-else-if="mode === $options.imageViewMode.onion"
v-bind="$props"/>
</div>
<div class="view-modes">
<ul class="view-modes-menu">
<li
:class="{
active: mode === $options.imageViewMode.twoup
}"
@click="changeMode($options.imageViewMode.twoup)">
{{ s__('ImageDiffViewer|2-up') }}
</li>
<li
:class="{
active: mode === $options.imageViewMode.swipe
}"
@click="changeMode($options.imageViewMode.swipe)">
{{ s__('ImageDiffViewer|Swipe') }}
</li>
<li
:class="{
active: mode === $options.imageViewMode.onion
}"
@click="changeMode($options.imageViewMode.onion)">
{{ s__('ImageDiffViewer|Onion skin') }}
</li>
</ul>
</div>
<div class="note-container"></div>
</div>
<div
v-else-if="diffMode === $options.diffModes.new"
class="diff-viewer added">
<image-viewer
:path="newPath"
:project-path="projectPath"
/>
</div>
<div
v-else
class="diff-viewer deleted">
<image-viewer
:path="oldPath"
:project-path="projectPath"
/>
</div>
</div>
</template>
<script> <script>
const buttonVariants = ['danger', 'primary', 'success', 'warning']; const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg']; const sizeVariants = ['sm', 'md', 'lg', 'xl'];
export default { export default {
name: 'GlModal', name: 'GlModal',
......
export function pixeliseValue(val) {
return val ? `${val}px` : '';
}
export default {};
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<div class="note-header"> <div class="note-header">
<div class="note-header-info"> <div class="note-header-info">
<a :href="getUserData.path"> <a :href="getUserData.path">
<span class="d-none d-sm-block">{{ getUserData.name }}</span> <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
<span class="note-headline-light">@{{ getUserData.username }}</span> <span class="note-headline-light">@{{ getUserData.username }}</span>
</a> </a>
</div> </div>
......
...@@ -41,7 +41,6 @@ export default { ...@@ -41,7 +41,6 @@ export default {
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
data-placement="left" data-placement="left"
data-container="body" data-container="body"
data-boundary="viewport"
@click="handleClick" @click="handleClick"
> >
<i <i
......
...@@ -34,7 +34,6 @@ export default { ...@@ -34,7 +34,6 @@ export default {
class="btn btn-blank gutter-toggle btn-sidebar-action" class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body" data-container="body"
data-placement="left" data-placement="left"
data-boundary="viewport"
@click="toggle" @click="toggle"
> >
<i <i
......
...@@ -293,3 +293,9 @@ input[type=color].form-control { ...@@ -293,3 +293,9 @@ input[type=color].form-control {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
} }
.project-templates-buttons {
.btn {
vertical-align: unset;
}
}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
.click-to-expand { .click-to-expand {
cursor: pointer; cursor: pointer;
vertical-align: initial;
} }
} }
} }
......
...@@ -444,10 +444,6 @@ img.emoji { ...@@ -444,10 +444,6 @@ img.emoji {
.break-word { .break-word {
word-wrap: break-word; word-wrap: break-word;
&.all-words {
word-break: break-word;
}
} }
/** COMMON CLASSES **/ /** COMMON CLASSES **/
......
...@@ -400,3 +400,51 @@ span.idiff { ...@@ -400,3 +400,51 @@ span.idiff {
color: $common-gray-light; color: $common-gray-light;
border: 1px solid $common-gray-light; border: 1px solid $common-gray-light;
} }
.preview-container {
height: 100%;
overflow: auto;
.file-container {
background-color: $gray-darker;
display: flex;
height: 100%;
align-items: center;
justify-content: center;
text-align: center;
.file-content {
padding: $gl-padding;
max-width: 100%;
max-height: 100%;
img {
max-width: 90%;
max-height: 70vh;
}
.is-zoomable {
cursor: pointer;
cursor: zoom-in;
&.is-zoomed {
cursor: pointer;
cursor: zoom-out;
max-width: none;
max-height: none;
margin-right: $gl-padding;
}
}
}
.file-info {
font-size: $label-font-size;
color: $diff-image-info-color;
}
}
.md-previewer {
padding: $gl-padding;
}
}
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
.gfm-color_chip { .gfm-color_chip {
display: inline-block; display: inline-block;
line-height: 1;
margin: 0 0 2px 4px; margin: 0 0 2px 4px;
vertical-align: middle; vertical-align: middle;
border-radius: 3px; border-radius: 3px;
......
...@@ -186,6 +186,7 @@ ...@@ -186,6 +186,7 @@
overflow-y: hidden; overflow-y: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
display: flex; display: flex;
flex-wrap: nowrap;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
......
.modal-xl {
max-width: 98%;
}
.modal-header { .modal-header {
background-color: $modal-body-bg; background-color: $modal-body-bg;
......
...@@ -257,6 +257,8 @@ ...@@ -257,6 +257,8 @@
} }
.scrolling-tabs-container { .scrolling-tabs-container {
position: relative;
.merge-request-tabs-container & { .merge-request-tabs-container & {
overflow: hidden; overflow: hidden;
} }
......
...@@ -58,7 +58,7 @@ table { ...@@ -58,7 +58,7 @@ table {
display: none; display: none;
} }
table, &,
tbody, tbody,
td { td {
display: block; display: block;
......
...@@ -868,3 +868,5 @@ $input-border-color: $theme-gray-200; ...@@ -868,3 +868,5 @@ $input-border-color: $theme-gray-200;
$input-color: $gl-text-color; $input-color: $gl-text-color;
$font-family-sans-serif: $regular_font; $font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font; $font-family-monospace: $monospace_font;
$input-line-height: 20px;
$btn-line-height: 20px;
...@@ -117,6 +117,7 @@ ...@@ -117,6 +117,7 @@
overflow-x: scroll; overflow-x: scroll;
white-space: nowrap; white-space: nowrap;
min-height: 200px; min-height: 200px;
display: flex;
@include media-breakpoint-only(sm) { @include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm}); height: calc(100vh - #{$issue-board-list-difference-sm});
...@@ -147,17 +148,15 @@ ...@@ -147,17 +148,15 @@
.board { .board {
display: inline-block; display: inline-block;
width: calc(85vw - 15px); flex: 1;
min-width: 300px;
max-width: 400px;
height: 100%; height: 100%;
padding-right: ($gl-padding / 2); padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2); padding-left: ($gl-padding / 2);
white-space: normal; white-space: normal;
vertical-align: top; vertical-align: top;
@include media-breakpoint-up(sm) {
width: 400px;
}
&.is-expandable { &.is-expandable {
.board-header { .board-header {
cursor: pointer; cursor: pointer;
...@@ -165,6 +164,8 @@ ...@@ -165,6 +164,8 @@
} }
&.is-collapsed { &.is-collapsed {
flex: none;
min-width: 0;
width: 50px; width: 50px;
.board-header { .board-header {
......
...@@ -189,8 +189,22 @@ ...@@ -189,8 +189,22 @@
img { img {
border: 1px solid $white-light; border: 1px solid $white-light;
background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), background-image: linear-gradient(
linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); 45deg,
$border-color 25%,
transparent 25%,
transparent 75%,
$border-color 75%,
$border-color 100%
),
linear-gradient(
45deg,
$border-color 25%,
transparent 25%,
transparent 75%,
$border-color 75%,
$border-color 100%
);
background-size: 10px 10px; background-size: 10px 10px;
background-position: 0 0, 5px 5px; background-position: 0 0, 5px 5px;
max-width: 100%; max-width: 100%;
...@@ -395,6 +409,69 @@ ...@@ -395,6 +409,69 @@
.line_content { .line_content {
white-space: pre-wrap; white-space: pre-wrap;
} }
.diff-file-container {
.frame.deleted {
border: 0;
background-color: inherit;
.image_file img {
border: 1px solid $deleted;
}
}
.frame.added {
border: 0;
background-color: inherit;
.image_file img {
border: 1px solid $added;
}
}
.swipe.view,
.onion-skin.view {
.swipe-wrap {
top: 0;
right: 0;
}
.frame.deleted {
top: 0;
right: 0;
}
.swipe-bar {
top: 0;
.top-handle {
top: -14px;
left: -7px;
}
.bottom-handle {
bottom: -14px;
left: -7px;
}
}
.file-container {
display: inline-block;
.file-content {
padding: 0;
img {
max-width: none;
}
}
}
}
.onion-skin.view .controls {
bottom: -25px;
}
}
} }
.file-content .diff-file { .file-content .diff-file {
...@@ -536,7 +613,7 @@ ...@@ -536,7 +613,7 @@
margin-right: 0; margin-right: 0;
border-color: $white-light; border-color: $white-light;
cursor: pointer; cursor: pointer;
transition: all .1s ease-out; transition: all 0.1s ease-out;
@for $i from 1 through 4 { @for $i from 1 through 4 {
&:nth-child(#{$i}) { &:nth-child(#{$i}) {
...@@ -563,7 +640,7 @@ ...@@ -563,7 +640,7 @@
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
padding: 0; padding: 0;
transition: transform .1s ease-out; transition: transform 0.1s ease-out;
z-index: 100; z-index: 100;
.collapse-icon { .collapse-icon {
...@@ -708,11 +785,35 @@ ...@@ -708,11 +785,35 @@
width: 100%; width: 100%;
height: 10px; height: 10px;
background-color: $white-light; background-color: $white-light;
background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), background-image: linear-gradient(
linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), 45deg,
linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), transparent,
linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%); transparent 73%,
background-position: 5px 5px,0 5px,0 5px,5px 5px; $diff-jagged-border-gradient-color 75%,
$white-light 80%
),
linear-gradient(
225deg,
transparent,
transparent 73%,
$diff-jagged-border-gradient-color 75%,
$white-light 80%
),
linear-gradient(
135deg,
transparent,
transparent 73%,
$diff-jagged-border-gradient-color 75%,
$white-light 80%
),
linear-gradient(
-45deg,
transparent,
transparent 73%,
$diff-jagged-border-gradient-color 75%,
$white-light 80%
);
background-position: 5px 5px, 0 5px, 0 5px, 5px 5px;
background-size: 10px 10px; background-size: 10px 10px;
background-repeat: repeat; background-repeat: repeat;
} }
...@@ -750,11 +851,16 @@ ...@@ -750,11 +851,16 @@
.frame.click-to-comment { .frame.click-to-comment {
position: relative; position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg') cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
// Retina cursor // Retina cursor
cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) cursor: -webkit-image-set(
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; image-url('illustrations/image_comment_light_cursor.svg') 1x,
image-url('illustrations/image_comment_light_cursor@2x.svg') 2x
)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
.comment-indicator { .comment-indicator {
position: absolute; position: absolute;
...@@ -840,7 +946,7 @@ ...@@ -840,7 +946,7 @@
.diff-notes-collapse, .diff-notes-collapse,
.note, .note,
.discussion-reply-holder, { .discussion-reply-holder {
display: none; display: none;
} }
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
.label-link { .label-link {
display: inline-flex; display: inline-flex;
vertical-align: top; vertical-align: text-bottom;
&:hover .color-label { &:hover .color-label {
text-decoration: underline; text-decoration: underline;
......
...@@ -670,6 +670,7 @@ ...@@ -670,6 +670,7 @@
.merge-request-tabs { .merge-request-tabs {
display: flex; display: flex;
flex-wrap: nowrap;
margin-bottom: 0; margin-bottom: 0;
padding: 0; padding: 0;
} }
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
} }
.table-holder { .table-holder {
overflow: unset;
width: 100%; width: 100%;
} }
......
...@@ -354,12 +354,6 @@ ...@@ -354,12 +354,6 @@
min-width: 200px; min-width: 200px;
} }
.deploy-keys {
.scrolling-tabs-container {
position: relative;
}
}
.deploy-key { .deploy-key {
// Ensure that the fingerprint does not overflow on small screens // Ensure that the fingerprint does not overflow on small screens
.fingerprint { .fingerprint {
...@@ -503,6 +497,12 @@ ...@@ -503,6 +497,12 @@
&:not(:first-child) { &:not(:first-child) {
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
.btn-template-icon {
position: absolute;
left: $gl-padding;
top: $gl-padding;
}
} }
.template-title { .template-title {
...@@ -520,12 +520,6 @@ ...@@ -520,12 +520,6 @@
} }
} }
svg {
position: absolute;
left: $gl-padding;
top: $gl-padding;
}
.project-fields-form { .project-fields-form {
display: none; display: none;
...@@ -536,34 +530,23 @@ ...@@ -536,34 +530,23 @@
} }
.template-input-group { .template-input-group {
position: relative; .input-group-prepend {
@include media-breakpoint-up(sm) {
display: flex;
}
.input-group-prepend,
.input-group-append {
flex: 1; flex: 1;
text-align: left;
padding-left: ($gl-padding * 3);
background-color: $white-light;
} }
.selected-template { .input-group-text {
line-height: 20px; width: 100%;
background-color: $white-light;
} }
.selected-icon { .selected-icon {
padding-right: $gl-padding;
svg { svg {
display: none; display: none;
top: 7px; top: 7px;
height: 20px; height: 20px;
width: 20px; width: 20px;
&.active {
display: block;
}
} }
} }
} }
......
...@@ -335,7 +335,6 @@ ...@@ -335,7 +335,6 @@
img { img {
max-width: 90%; max-width: 90%;
max-height: 90%;
} }
.isZoomable { .isZoomable {
...@@ -553,6 +552,10 @@ ...@@ -553,6 +552,10 @@
} }
.multi-file-commit-list-item { .multi-file-commit-list-item {
&.is-active {
background-color: $white-normal;
}
.multi-file-discard-btn { .multi-file-discard-btn {
display: none; display: none;
margin-top: -2px; margin-top: -2px;
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
.settings-content { .settings-content {
max-height: 1px; max-height: 1px;
overflow-y: scroll; overflow-y: hidden;
padding-right: 110px; padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out; animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it // Keep the section from expanding when we scroll over it
......
...@@ -107,12 +107,12 @@ ...@@ -107,12 +107,12 @@
} }
.performance-bar-modal { .performance-bar-modal {
.modal-footer { .modal-body {
display: none; padding: 0;
} }
.modal-dialog { .modal-footer {
width: 860px; display: none;
} }
} }
} }
......
...@@ -292,8 +292,10 @@ class ApplicationController < ActionController::Base ...@@ -292,8 +292,10 @@ class ApplicationController < ActionController::Base
return unless current_user return unless current_user
return if current_user.terms_accepted? return if current_user.terms_accepted?
message = _("Please accept the Terms of Service before continuing.")
if sessionless_user? if sessionless_user?
render_403 access_denied!(message)
else else
# Redirect to the destination if the request is a get. # Redirect to the destination if the request is a get.
# Redirect to the source if it was a post, so the user can re-submit after # Redirect to the source if it was a post, so the user can re-submit after
...@@ -304,7 +306,7 @@ class ApplicationController < ActionController::Base ...@@ -304,7 +306,7 @@ class ApplicationController < ActionController::Base
URI(request.referer).path if request.referer URI(request.referer).path if request.referer
end end
flash[:notice] = _("Please accept the Terms of Service before continuing.") flash[:notice] = message
redirect_to terms_path(redirect: redirect_path), status: :found redirect_to terms_path(redirect: redirect_path), status: :found
end end
end end
......
...@@ -24,6 +24,10 @@ module InternalRedirect ...@@ -24,6 +24,10 @@ module InternalRedirect
nil nil
end end
def sanitize_redirect(url_or_path)
safe_redirect_path(url_or_path) || safe_redirect_path_for_url(url_or_path)
end
def host_allowed?(uri) def host_allowed?(uri)
uri.host == request.host && uri.host == request.host &&
uri.port == request.port uri.port == request.port
......
...@@ -197,15 +197,14 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -197,15 +197,14 @@ class Projects::BlobController < Projects::ApplicationController
end end
def show_json def show_json
json = blob_json(@blob) set_last_commit_sha
return render_404 unless json
path_segments = @path.split('/') path_segments = @path.split('/')
path_segments.pop path_segments.pop
tree_path = path_segments.join('/') tree_path = path_segments.join('/')
render json: json.merge( json = {
id: @blob.id, id: @blob.id,
last_commit_sha: @last_commit_sha,
path: blob.path, path: blob.path,
name: blob.name, name: blob.name,
extension: blob.extension, extension: blob.extension,
...@@ -221,6 +220,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -221,6 +220,10 @@ class Projects::BlobController < Projects::ApplicationController
commits_path: project_commits_path(project, @id), commits_path: project_commits_path(project, @id),
tree_path: project_tree_path(project, File.join(@ref, tree_path)), tree_path: project_tree_path(project, File.join(@ref, tree_path)),
permalink: project_blob_path(project, File.join(@commit.id, @path)) permalink: project_blob_path(project, File.join(@commit.id, @path))
) }
json.merge!(blob_json(@blob) || {}) unless params[:viewer] == 'none'
render json: json
end end
end end
...@@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder ...@@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder
end end
def authorized_snippets def authorized_snippets
Snippet.where(feature_available_projects.or(not_project_related)) # This query was intentionally converted to a raw one to get it work in Rails 5.0.
# In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
# Please convert it back when on rails 5.2 as it works again as expected since 5.2.
Snippet.where("#{feature_available_projects} OR #{not_project_related}")
.public_or_visible_to_user(current_user) .public_or_visible_to_user(current_user)
end end
...@@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder ...@@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder
def feature_available_projects def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project # Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)
projects = projects_for_user do |part| projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user) part.with_feature_available_for_user(:snippets, current_user)
end.select(:id) end.select(:id)
arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql) # This query was intentionally converted to a raw one to get it work in Rails 5.0.
table[:project_id].in(arel_query) # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
# Please convert it back when on rails 5.2 as it works again as expected since 5.2.
"snippets.project_id IN (#{projects.to_sql})"
end end
def not_project_related def not_project_related
table[:project_id].eq(nil) table[:project_id].eq(nil).to_sql
end end
def table def table
......
...@@ -173,11 +173,12 @@ module ProjectsHelper ...@@ -173,11 +173,12 @@ module ProjectsHelper
key = [ key = [
project.route.cache_key, project.route.cache_key,
project.cache_key, project.cache_key,
project.last_activity_date,
controller.controller_name, controller.controller_name,
controller.action_name, controller.action_name,
Gitlab::CurrentSettings.cache_key, Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}", "cross-project:#{can?(current_user, :read_cross_project)}",
'v2.5' 'v2.6'
] ]
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
......
...@@ -114,7 +114,7 @@ module CacheMarkdownField ...@@ -114,7 +114,7 @@ module CacheMarkdownField
end end
def latest_cached_markdown_version def latest_cached_markdown_version
return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version return CacheMarkdownField::CACHE_COMMONMARK_VERSION unless cached_markdown_version
if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
CacheMarkdownField::CACHE_REDCARPET_VERSION CacheMarkdownField::CACHE_REDCARPET_VERSION
......
...@@ -72,7 +72,7 @@ class Project < ActiveRecord::Base ...@@ -72,7 +72,7 @@ class Project < ActiveRecord::Base
add_authentication_token_field :runners_token add_authentication_token_field :runners_token
before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) } before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token before_save :ensure_runners_token
......
...@@ -43,13 +43,18 @@ class GemnasiumService < Service ...@@ -43,13 +43,18 @@ class GemnasiumService < Service
def execute(data) def execute(data)
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
# Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010
repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.path_to_repo
end
Gemnasium::GitlabService.execute( Gemnasium::GitlabService.execute(
ref: data[:ref], ref: data[:ref],
before: data[:before], before: data[:before],
after: data[:after], after: data[:after],
token: token, token: token,
api_key: api_key, api_key: api_key,
repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9 repo: repo_path
) )
end end
end end
...@@ -562,6 +562,17 @@ module QuickActions ...@@ -562,6 +562,17 @@ module QuickActions
end end
end end
desc 'Make issue confidential.'
explanation do
'Makes this issue confidential'
end
condition do
issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
command :confidential do
@updates[:confidential] = true
end
def extract_users(params) def extract_users(params)
return [] if params.nil? return [] if params.nil?
......
...@@ -73,6 +73,15 @@ module ObjectStorage ...@@ -73,6 +73,15 @@ module ObjectStorage
upload.id) upload.id)
end end
def exclusive_lease_key
# For FileUploaders, model may have many uploaders. In that case
# we want to use exclusive key per upload, not per model to allow
# parallel migration
key_object = upload || model
"object_storage_migrate:#{key_object.class}:#{key_object.id}"
end
private private
def current_upload_satisfies?(paths, model) def current_upload_satisfies?(paths, model)
...@@ -316,6 +325,10 @@ module ObjectStorage ...@@ -316,6 +325,10 @@ module ObjectStorage
super super
end end
def exclusive_lease_key
"object_storage_migrate:#{model.class}:#{model.id}"
end
private private
def schedule_background_upload? def schedule_background_upload?
...@@ -382,10 +395,6 @@ module ObjectStorage ...@@ -382,10 +395,6 @@ module ObjectStorage
end end
end end
def exclusive_lease_key
"object_storage_migrate:#{model.class}:#{model.id}"
end
def with_exclusive_lease def with_exclusive_lease
lease_key = exclusive_lease_key lease_key = exclusive_lease_key
uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain
......
...@@ -12,9 +12,9 @@ ...@@ -12,9 +12,9 @@
- if can?(current_user, :award_emoji, awardable) - if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder .award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button', %button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add reaction', 'aria-label': _('Add reaction'),
class: ("js-user-authored" if user_authored), class: ("js-user-authored" if user_authored),
data: { title: 'Add reaction', placement: "bottom" } } data: { title: _('Add reaction'), placement: "bottom" } }
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
......
- message = local_assigns.fetch(:message) - message = local_assigns.fetch(:message, nil)
- content_for(:title, 'Access Denied') - content_for(:title, 'Access Denied')
= image_tag('illustrations/error-403.svg', alt: '403', lazy: false) = image_tag('illustrations/error-403.svg', alt: '403', lazy: false)
......
...@@ -10,16 +10,18 @@ ...@@ -10,16 +10,18 @@
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview
.project-fields-form .project-fields-form
.form-group .row
%label.label-light .form-group.col-sm-12
Template %label.label-light
.input-group.template-input-group Template
.input-group-prepend .input-group.template-input-group
.input-group-text .input-group-prepend
.selected-icon .input-group-text
- Gitlab::ProjectTemplate.all.each do |template| .selected-icon
= custom_icon(template.logo) - Gitlab::ProjectTemplate.all.each do |template|
.selected-template = custom_icon(template.logo)
%button.btn.btn-default.change-template{ type: "button" } Change template .selected-template
.input-group-append
%button.btn.btn-default.change-template{ type: "button" } Change template
= render 'new_project_fields', f: f, project_name_id: "template-project-name" = render 'new_project_fields', f: f, project_name_id: "template-project-name"
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
- anchors.each do |anchor| - anchors.each do |anchor|
%li.nav-item %li.nav-item
= link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
%span.stat-text= anchor.label .stat-text= anchor.label
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) - url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed. This diff is collapsed.
%a.click-to-expand Click to expand it. %button.click-to-expand.btn.btn-link Click to expand it.
...@@ -44,14 +44,14 @@ ...@@ -44,14 +44,14 @@
.git-empty .git-empty
%fieldset %fieldset
%h5 Git global setup %h5 Git global setup
%pre.card.bg-light %pre.bg-light
:preserve :preserve
git config --global user.name "#{h git_user_name}" git config --global user.name "#{h git_user_name}"
git config --global user.email "#{h git_user_email}" git config --global user.email "#{h git_user_email}"
%fieldset %fieldset
%h5 Create a new repository %h5 Create a new repository
%pre.card.bg-light %pre.bg-light
:preserve :preserve
git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
cd #{h @project.path} cd #{h @project.path}
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
%fieldset %fieldset
%h5 Existing folder %h5 Existing folder
%pre.card.bg-light %pre.bg-light
:preserve :preserve
cd existing_folder cd existing_folder
git init git init
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
%fieldset %fieldset
%h5 Existing Git repository %h5 Existing Git repository
%pre.card.bg-light %pre.bg-light
:preserve :preserve
cd existing_repo cd existing_repo
git remote rename origin old-origin git remote rename origin old-origin
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
= custom_icon ('illustration_no_commits') = custom_icon ('illustration_no_commits')
- else - else
%ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active %li.commits-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits Commits
%span.badge.badge-pill= @commits.size %span.badge.badge-pill= @commits.size
......
...@@ -32,26 +32,25 @@ ...@@ -32,26 +32,25 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
.nav-links.scrolling-tabs.nav.nav-tabs %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%ul.merge-request-tabs.nav-tabs.nav %li.notes-tab
%li.notes-tab = tab_link_for @merge_request, :show, force_link: @commit.present? do
= tab_link_for @merge_request, :show, force_link: @commit.present? do Discussion
Discussion %span.badge.badge-pill= @merge_request.related_notes.user.count
%span.badge.badge-pill= @merge_request.related_notes.user.count - if @merge_request.source_project
- if @merge_request.source_project %li.commits-tab
%li.commits-tab = tab_link_for @merge_request, :commits do
= tab_link_for @merge_request, :commits do Commits
Commits %span.badge.badge-pill= @commits_count
%span.badge.badge-pill= @commits_count - if @pipelines.any?
- if @pipelines.any? %li.pipelines-tab
%li.pipelines-tab = tab_link_for @merge_request, :pipelines do
= tab_link_for @merge_request, :pipelines do Pipelines
Pipelines %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size %li.diffs-tab
%li.diffs-tab = tab_link_for @merge_request, :diffs do
= tab_link_for @merge_request, :diffs do Changes
Changes %span.badge.badge-pill= @merge_request.diff_size
%span.badge.badge-pill= @merge_request.diff_size
- if has_vue_discussions_cookie? - if has_vue_discussions_cookie?
#js-vue-discussion-counter #js-vue-discussion-counter
......
%pre.build-trace#build-trace %pre.build-trace#build-trace
%code.bash.js-build-output %code.bash.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
.issuable-form-select-holder .issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
= render "shared/issuable/form/weight", issuable: issuable, form: form = render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
- if has_due_date - if has_due_date
.col-lg-6 .col-lg-6
......
...@@ -6,12 +6,6 @@ class GitGarbageCollectWorker ...@@ -6,12 +6,6 @@ class GitGarbageCollectWorker
# Timeout set to 24h # Timeout set to 24h
LEASE_TIMEOUT = 86400 LEASE_TIMEOUT = 86400
GITALY_MIGRATED_TASKS = {
gc: :garbage_collect,
full_repack: :repack_full,
incremental_repack: :repack_incremental
}.freeze
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id) project = Project.find(project_id)
active_uuid = get_lease_uuid(lease_key) active_uuid = get_lease_uuid(lease_key)
...@@ -27,21 +21,7 @@ class GitGarbageCollectWorker ...@@ -27,21 +21,7 @@ class GitGarbageCollectWorker
end end
task = task.to_sym task = task.to_sym
cmd = command(task) gitaly_call(task, project.repository.raw_repository)
gitaly_migrate(GITALY_MIGRATED_TASKS[task], status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_call(task, project.repository.raw_repository)
else
repo_path = project.repository.path_to_repo
description = "'#{cmd.join(' ')}' in #{repo_path}"
Gitlab::GitLogger.info(description)
output, status = Gitlab::Popen.popen(cmd, repo_path)
Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
end
end
# Refresh the branch cache in case garbage collection caused a ref lookup to fail # Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc flush_ref_caches(project) if task == :gc
...@@ -82,21 +62,12 @@ class GitGarbageCollectWorker ...@@ -82,21 +62,12 @@ class GitGarbageCollectWorker
when :incremental_repack when :incremental_repack
client.repack_incremental client.repack_incremental
end end
end rescue GRPC::NotFound => e
Gitlab::GitLogger.error("#{method} failed:\nRepository not found")
def command(task) raise Gitlab::Git::Repository::NoRepository.new(e)
case task rescue GRPC::BadStatus => e
when :gc Gitlab::GitLogger.error("#{method} failed:\n#{e}")
git(write_bitmaps: bitmaps_enabled?) + %w[gc] raise Gitlab::Git::CommandError.new(e)
when :full_repack
git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects]
when :incremental_repack
# Normal git repack fails when bitmaps are enabled. It is impossible to
# create a bitmap here anyway.
git(write_bitmaps: false) + %w[repack -d]
else
raise "Invalid gc task: #{task.inspect}"
end
end end
def flush_ref_caches(project) def flush_ref_caches(project)
...@@ -108,19 +79,4 @@ class GitGarbageCollectWorker ...@@ -108,19 +79,4 @@ class GitGarbageCollectWorker
def bitmaps_enabled? def bitmaps_enabled?
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
end end
def git(write_bitmaps:)
config_value = write_bitmaps ? 'true' : 'false'
%W[git -c repack.writeBitmaps=#{config_value}]
end
def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
Gitlab::GitLogger.error("#{method} failed:\nRepository not found")
raise Gitlab::Git::Repository::NoRepository.new(e)
rescue GRPC::BadStatus => e
Gitlab::GitLogger.error("#{method} failed:\n#{e}")
raise Gitlab::Git::CommandError.new(e)
end
end end
---
title: Expose visibility via Snippets API
merge_request: 19620
author: Jan Beckmann
type: added
---
title: 'Fix username validation order on signup, resolves #45575'
merge_request: 19610
author: Jan Beckmann
type: fixed
---
title: Make quick commands case insensitive
merge_request: 19614
author: Jan Beckmann
type: fixed
---
title: Add /confidential quick action
merge_request:
author: Jan Beckmann
type: added
---
title: Use upload ID for creating lease key for file uploaders.
merge_request:
author:
type: fixed
---
title: Line height fixed
merge_request:
author: Murat Dogan
type: fixed
---
title: Fix active tab highlight when creating new merge request
merge_request: 19781
author: Jan Beckmann
type: fixed
---
title: Fix fields for author & assignee in MR API docs.
merge_request: 19798
author: gfyoung
type: fixed
---
title: "[Rails5] Fix snippets_finder arel queries"
merge_request: 19796
author: "@blackst0ne"
type: fixed
---
title: Use CommonMark syntax and rendering for new Markdown content
merge_request: 19331
author:
type: added
---
title: 'Remove incorrect CI doc re: PowerShell'
merge_request: 19622
author: gfyoung
type: fixed
---
title: Add support for verifying remote uploads, artifacts, and LFS objects in check rake tasks
merge_request: 19501
author:
type: added
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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