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
ee/db/**/*
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/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
......
......@@ -370,10 +370,10 @@ package-and-qa:
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-omnibus
SCRIPT_NAME: trigger-build
retry: 0
script:
- ./$SCRIPT_NAME
- ./$SCRIPT_NAME omnibus
when: manual
only:
- //@gitlab-org/gitlab-ce
......
......@@ -487,7 +487,7 @@ Style/EmptyLiteral:
- 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/gitaly_client.rb'
- 'scripts/trigger-build-omnibus'
- 'scripts/trigger-build'
- 'spec/features/merge_requests/versions_spec.rb'
- 'spec/helpers/merge_requests_helper_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]
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.
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
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
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.
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
existing issue or decide to create a whole new issue for the purpose of development.
To prevent the misunderstanding that a feature will be be delivered in the
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
......
......@@ -177,6 +177,7 @@ the stable branch are:
* Fixes for [regressions](#regressions)
* Fixes for security issues
* 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)
During the feature freeze all merge requests that are meant to go into the
......
......@@ -119,7 +119,7 @@ const gfmRules = {
return el.outerHTML;
},
'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,
// or leave the line entirely blank.
lines = lines.map((l) => {
......@@ -129,9 +129,13 @@ const gfmRules = {
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();
return `<${tag}>${text}</${tag}>`;
},
......@@ -215,22 +219,22 @@ const gfmRules = {
return text.replace(/^- /mg, '1. ');
},
'h1'(el, text) {
return `# ${text.trim()}`;
return `# ${text.trim()}\n`;
},
'h2'(el, text) {
return `## ${text.trim()}`;
return `## ${text.trim()}\n`;
},
'h3'(el, text) {
return `### ${text.trim()}`;
return `### ${text.trim()}\n`;
},
'h4'(el, text) {
return `#### ${text.trim()}`;
return `#### ${text.trim()}\n`;
},
'h5'(el, text) {
return `##### ${text.trim()}`;
return `##### ${text.trim()}\n`;
},
'h6'(el, text) {
return `###### ${text.trim()}`;
return `###### ${text.trim()}\n`;
},
'strong'(el, text) {
return `**${text}**`;
......@@ -241,11 +245,13 @@ const gfmRules = {
'del'(el, text) {
return `~~${text}~~`;
},
'sup'(el, text) {
return `^${text}`;
},
'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) {
const theadEl = el.querySelector('thead');
......@@ -263,7 +269,9 @@ const gfmRules = {
let before = '';
let after = '';
switch (cell.style.textAlign) {
const alignment = cell.align || cell.style.textAlign;
switch (alignment) {
case 'center':
before = ':';
after = ':';
......
......@@ -66,8 +66,14 @@ export default class CreateMergeRequestDropdown {
}
bindEvents() {
this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
this.createMergeRequestButton.addEventListener(
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
this.createTargetButton.addEventListener(
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
......@@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown {
checkAbilityToCreateBranch() {
this.setUnavailableButtonState();
axios.get(this.canCreatePath)
axios
.get(this.canCreatePath)
.then(({ data }) => {
this.setUnavailableButtonState(false);
......@@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown {
createBranch() {
this.isCreatingBranch = true;
return axios.post(this.createBranchPath)
return axios
.post(this.createBranchPath)
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
......@@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown {
createMergeRequest() {
this.isCreatingMergeRequest = true;
return axios.post(this.createMrPath)
return axios
.post(this.createMrPath)
.then(({ data }) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
......@@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
return axios.get(this.refsPath + ref)
return axios
.get(`${this.refsPath}${encodeURIComponent(ref)}`)
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
......@@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown {
if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
result =
CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
......@@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown {
}
isBusy() {
return this.isCreatingMergeRequest ||
return (
this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
this.branchCreated ||
this.isGettingRef;
this.isGettingRef
);
}
onChangeInput(event) {
......@@ -271,7 +284,8 @@ export default class CreateMergeRequestDropdown {
value = this.branchInput.value;
} else if (event.target === this.refInput) {
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);
} else {
return false;
......@@ -396,7 +410,8 @@ export default class CreateMergeRequestDropdown {
showNotAvailableMessage(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);
input.classList.add('gl-field-error-outline');
......@@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown {
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
const pathReplacement = `$1${encodeURIComponent(ref)}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
this.createBranchPath = this.createBranchPath.replace(
this.regexps[target].createBranchPath,
pathReplacement,
);
this.createMrPath = this.createMrPath.replace(
this.regexps[target].createMrPath,
pathReplacement,
);
}
}
......@@ -43,6 +43,15 @@ export default {
required: false,
default: false,
},
activeFileKey: {
type: String,
required: false,
default: null,
},
keyPrefix: {
type: String,
required: true,
},
},
data() {
return {
......@@ -113,8 +122,9 @@ export default {
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:key-prefix="keyPrefix"
:staged-list="stagedList"
:active-file-key="activeFileKey"
/>
</li>
</ul>
......
......@@ -30,6 +30,11 @@ export default {
required: false,
default: false,
},
activeFileKey: {
type: String,
required: false,
default: null,
},
},
computed: {
iconName() {
......@@ -39,6 +44,12 @@ export default {
iconClass() {
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: {
...mapActions([
......@@ -51,7 +62,7 @@ export default {
openFileInEditor() {
return this.openPendingTab({
file: this.file,
keyPrefix: this.keyPrefix.toLowerCase(),
keyPrefix: this.keyPrefix,
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer(viewerTypes.diff);
......@@ -70,7 +81,12 @@ export default {
</script>
<template>
<div class="multi-file-commit-list-item">
<div
:class="{
'is-active': isActive
}"
class="multi-file-commit-list-item"
>
<button
type="button"
class="multi-file-commit-list-path"
......
......@@ -94,7 +94,7 @@ export default {
<p class="append-bottom-0">
{{ __('Found errors in your .gitlab-ci.yml:') }}
</p>
<p class="append-bottom-0">
<p class="append-bottom-0 break-word">
{{ latestPipeline.yamlError }}
</p>
<p
......
......@@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants';
import { activityBarViews } from '../constants';
import { activityBarViews, stageKeys } from '../constants';
export default {
components: {
......@@ -27,11 +27,14 @@ export default {
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
},
},
watch: {
hasChanges() {
......@@ -44,6 +47,7 @@ export default {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
})
.then(changeViewer => {
if (changeViewer) {
......@@ -62,6 +66,7 @@ export default {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
stageKeys,
};
</script>
......@@ -86,8 +91,10 @@ export default {
>
<commit-files-list
:title="__('Unstaged')"
:key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles"
:action-btn-text="__('Stage all')"
:active-file-key="activeFileKey"
class="is-first"
icon-name="unstaged"
action="stageAllChanges"
......@@ -95,9 +102,11 @@ export default {
/>
<commit-files-list
:title="__('Staged')"
:key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles"
:action-btn-text="__('Unstage all')"
:staged-list="true"
:active-file-key="activeFileKey"
icon-name="staged"
action="unstageAllChanges"
item-action-component="unstage-button"
......
......@@ -2,6 +2,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
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 Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
......@@ -9,6 +10,7 @@ import ExternalLink from './external_link.vue';
export default {
components: {
ContentViewer,
DiffViewer,
ExternalLink,
},
props: {
......@@ -18,7 +20,13 @@ export default {
},
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapState([
'rightPanelCollapsed',
'viewer',
'panelResizing',
'currentActivityView',
'rightPane',
]),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
......@@ -29,9 +37,18 @@ export default {
shouldHideEditor() {
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() {
return {
active: this.file.viewMode === 'edit',
active: this.file.viewMode === 'editor',
};
},
previewTabCSS() {
......@@ -53,7 +70,7 @@ export default {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
viewMode: 'editor',
});
}
}
......@@ -62,7 +79,7 @@ export default {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
viewMode: 'editor',
});
}
},
......@@ -77,6 +94,9 @@ export default {
this.editor.updateDimensions();
}
},
rightPane() {
this.editor.updateDimensions();
},
},
beforeDestroy() {
this.editor.dispose();
......@@ -197,7 +217,7 @@ export default {
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })">
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
......@@ -222,7 +242,7 @@ export default {
/>
</div>
<div
v-show="!shouldHideEditor && file.viewMode === 'edit'"
v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor"
:class="{
'is-readonly': isCommitModeActive,
......@@ -231,10 +251,18 @@ export default {
>
</div>
<content-viewer
v-if="shouldHideEditor || file.viewMode === 'preview'"
v-if="showContentViewer"
:content="file.content || file.raw"
:path="file.rawPath || file.path"
:file-size="file.size"
: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>
</template>
......@@ -21,7 +21,19 @@ export const viewerTypes = {
diff: 'diff',
};
export const diffModes = {
replaced: 'replaced',
new: 'new',
deleted: 'deleted',
renamed: 'renamed',
};
export const rightSidebarViews = {
pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail',
};
export const stageKeys = {
unstaged: 'unstaged',
staged: 'staged',
};
......@@ -9,7 +9,7 @@ export default {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getFileData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } });
},
getRawFileData(file) {
if (file.tempFile) {
......
......@@ -49,31 +49,6 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
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 }) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
......@@ -128,24 +103,17 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
const payload = createCommitPayload({
branch: getters.branchName,
newBranch,
state,
rootState,
});
commit(types.UPDATE_LOADING, true);
return getCommitStatus
.then(
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))
return service
.commit(rootState.currentProjectId, payload)
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
......@@ -220,12 +188,16 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
);
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
if (err.response.status === 400) {
$('#ide-create-branch-modal').modal('show');
} 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);
});
......
......@@ -31,15 +31,16 @@ export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
dispatch('pipelines/resetLatestPipeline', null, { root: true });
dispatch('setCurrentBranchId', '', { root: true });
dispatch('pipelines/stopPipelinePolling', null, { root: true })
.then(() => {
dispatch('pipelines/resetLatestPipeline', null, { root: true });
dispatch('pipelines/clearEtagPoll', null, { root: true });
})
.catch(e => {
throw e;
});
dispatch('setRightPane', null, { root: true });
router.push(`/project/${projectPath}/merge_requests/${id}`);
};
......
......@@ -106,7 +106,9 @@ export const fetchJobTrace = ({ dispatch, state }) => {
.catch(() => dispatch('receiveJobTraceError'));
};
export const resetLatestPipeline = ({ commit }) =>
export const resetLatestPipeline = ({ commit }) => {
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
commit(types.SET_DETAIL_JOB, null);
};
export default () => {};
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
import { diffModes } from '../../constants';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
......@@ -46,6 +47,7 @@ export default {
baseRaw: null,
html: data.html,
size: data.size,
lastCommitSha: data.last_commit_sha,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......@@ -85,8 +87,19 @@ export default {
});
},
[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], {
mrChange,
mrChange: {
...mrChange,
diffMode,
},
});
},
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
......
......@@ -17,6 +17,7 @@ export const dataStructure = () => ({
changed: false,
staged: false,
lastCommitPath: '',
lastCommitSha: '',
lastCommit: {
id: '',
url: '',
......@@ -39,7 +40,7 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
viewMode: 'edit',
viewMode: 'editor',
previewMode: null,
size: 0,
parentPath: null,
......@@ -104,7 +105,7 @@ export const setPageTitle = title => {
document.title = title;
};
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({
branch,
commit_message: state.commitMessage,
actions: rootState.stagedFiles.map(f => ({
......@@ -112,6 +113,7 @@ export const createCommitPayload = (branch, newBranch, state, rootState) => ({
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch ? undefined : f.lastCommitSha,
})),
start_branch: newBranch ? rootState.currentBranchId : undefined,
});
......
/* eslint-disable no-param-reassign, comma-dangle */
/* eslint-disable no-param-reassign */
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.inlineConflictLines = Vue.extend({
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
mixins: [utilsMixin, actionsMixin],
props: {
file: {
type: Object,
......
/* eslint-disable no-param-reassign, comma-dangle */
import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';
import utilsMixin from '../mixins/line_conflict_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
mixins: [utilsMixin, actionsMixin],
props: {
file: {
type: Object,
......
/* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
class mergeConflictsService {
constructor(options) {
this.conflictsPath = options.conflictsPath;
this.resolveConflictsPath = options.resolveConflictsPath;
}
fetchConflictsData() {
return axios.get(this.conflictsPath);
}
export default class MergeConflictsService {
constructor(options) {
this.conflictsPath = options.conflictsPath;
this.resolveConflictsPath = options.resolveConflictsPath;
}
submitResolveConflicts(data) {
return axios.post(this.resolveConflictsPath, data);
}
fetchConflictsData() {
return axios.get(this.conflictsPath);
}
global.mergeConflicts.mergeConflictsService = mergeConflictsService;
})(window.gl || (window.gl = {}));
submitResolveConflicts(data) {
return axios.post(this.resolveConflictsPath, data);
}
}
/* eslint-disable new-cap, comma-dangle, no-new */
import $ from 'jquery';
import Vue from 'vue';
import Flash from '../flash';
import createFlash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
import './mixins/line_conflict_utils';
import './mixins/line_conflict_actions';
import MergeConflictsService from './merge_conflict_service';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
......@@ -17,9 +13,9 @@ export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
});
initIssuableSidebar();
......@@ -29,17 +25,26 @@ export default function initMergeConflicts() {
components: {
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines,
},
data: mergeConflictsStore.state,
computed: {
conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
conflictsCountText() {
return mergeConflictsStore.getConflictsCountText();
},
readyToCommit() {
return mergeConflictsStore.isReadyToCommit();
},
commitButtonText() {
return mergeConflictsStore.getCommitButtonText();
},
showDiffViewTypeSwitcher() {
return mergeConflictsStore.fileTextTypePresent();
},
},
created() {
mergeConflictsService.fetchConflictsData()
mergeConflictsService
.fetchConflictsData()
.then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
......@@ -87,9 +92,9 @@ export default function initMergeConflicts() {
})
.catch(() => {
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 */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.actions = {
methods: {
handleSelected(file, sectionId, selection) {
gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
}
}
};
})(window.gl || (window.gl = {}));
export default {
methods: {
handleSelected(file, sectionId, selection) {
gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
},
},
};
/* eslint-disable no-param-reassign, quote-props, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.utils = {
methods: {
lineCssClass(line) {
return {
'head': line.isHead,
'origin': line.isOrigin,
'match': line.hasMatch,
'selected': line.isSelected,
'unselected': line.isUnselected
};
}
}
};
})(window.gl || (window.gl = {}));
export default {
methods: {
lineCssClass(line) {
return {
head: line.isHead,
origin: line.isOrigin,
match: line.hasMatch,
selected: line.isSelected,
unselected: line.isUnselected,
};
},
},
};
......@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
$('.mr-loading-status .loading').toggleClass('hidden', !status);
$('.mr-loading-status .loading').toggleClass('hide', !status);
}
diffViewType() {
......
......@@ -56,7 +56,7 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
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>';
}
......
......@@ -1675,7 +1675,7 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
<span class="d-none d-sm-block">${_.escape(
<span class="d-none d-sm-inline-block">${_.escape(
currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
......@@ -1694,7 +1694,7 @@ export default class Notes {
</li>`,
);
$tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname));
$tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
$tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
......
......@@ -62,13 +62,13 @@ export default class UsernameValidator {
return this.setPendingState();
}
if (!this.state.available) {
return this.setUnavailableState();
}
if (!this.state.valid) {
return this.setInvalidState();
}
if (!this.state.available) {
return this.setUnavailableState();
}
}
interceptInvalid(event) {
......@@ -89,7 +89,6 @@ export default class UsernameValidator {
setAvailabilityState(usernameTaken) {
if (usernameTaken) {
this.state.valid = false;
this.state.available = false;
} else {
this.state.available = true;
......
......@@ -180,7 +180,7 @@ export default class UserTabs {
}
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status);
return this.$parentEl.find('.loading-status .loading').toggleClass('hide', !status);
}
setCurrentAction(source) {
......
......@@ -56,7 +56,7 @@ export default {
<gl-modal
:id="`modal-peek-${metric}-details`"
:header-title-text="header"
modal-size="lg"
modal-size="xl"
class="performance-bar-modal"
>
<table
......@@ -71,7 +71,7 @@ export default {
<td
v-for="key in keys"
:key="key"
class="break-word all-words"
class="break-word"
>
{{ item[key] }}
</td>
......
......@@ -90,7 +90,7 @@ const bindEvents = () => {
function chooseTemplate() {
$('.template-option').hide();
$projectFieldsForm.addClass('selected');
$selectedIcon.removeClass('active');
$selectedIcon.removeClass('d-block');
const value = $(this).val();
const templates = {
rails: {
......@@ -109,7 +109,7 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
$(selectedTemplate.icon).addClass('active');
$(selectedTemplate.icon).addClass('d-block');
$templateProjectNameInput.focus();
}
......
......@@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>';
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 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 {
constructor(file) {
......
......@@ -32,7 +32,10 @@ export default {
<div class="file-container">
<div class="file-content">
<p class="prepend-top-10 file-info">
{{ fileName }} ({{ fileSizeReadable }})
{{ fileName }}
<template v-if="fileSize > 0">
({{ fileSizeReadable }})
</template>
</p>
<a
:href="path"
......
<script>
import _ from 'underscore';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
......@@ -12,6 +13,10 @@ export default {
required: false,
default: 0,
},
renderInfo: {
type: Boolean,
default: true,
},
},
data() {
return {
......@@ -26,14 +31,34 @@ export default {
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: {
onImgLoad() {
const contentImg = this.$refs.contentImg;
this.isZoomable =
contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
if (contentImg) {
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() {
if (this.isZoomable) this.isZoomed = !this.isZoomed;
......@@ -47,20 +72,22 @@ export default {
<div class="file-content image_file">
<img
ref="contentImg"
:class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }"
:class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path"
:alt="path"
@load="onImgLoad"
@click="onImgClick"/>
<p class="file-info prepend-top-10">
<p
v-if="renderInfo"
class="file-info prepend-top-10">
<template v-if="fileSize>0">
{{ fileSizeReadable }}
</template>
<template v-if="fileSize>0 && width && height">
-
|
</template>
<template v-if="width && height">
{{ width }} x {{ height }}
W: {{ width }} | H: {{ height }}
</template>
</p>
</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>
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
const sizeVariants = ['sm', 'md', 'lg'];
const sizeVariants = ['sm', 'md', 'lg', 'xl'];
export default {
name: 'GlModal',
......
export function pixeliseValue(val) {
return val ? `${val}px` : '';
}
export default {};
......@@ -54,7 +54,7 @@
<div class="note-header">
<div class="note-header-info">
<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>
</a>
</div>
......
......@@ -41,7 +41,6 @@ export default {
class="sidebar-collapsed-icon"
data-placement="left"
data-container="body"
data-boundary="viewport"
@click="handleClick"
>
<i
......
......@@ -34,7 +34,6 @@ export default {
class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body"
data-placement="left"
data-boundary="viewport"
@click="toggle"
>
<i
......
......@@ -293,3 +293,9 @@ input[type=color].form-control {
color: $gl-text-color-secondary;
}
}
.project-templates-buttons {
.btn {
vertical-align: unset;
}
}
......@@ -16,6 +16,7 @@
.click-to-expand {
cursor: pointer;
vertical-align: initial;
}
}
}
......
......@@ -444,10 +444,6 @@ img.emoji {
.break-word {
word-wrap: break-word;
&.all-words {
word-break: break-word;
}
}
/** COMMON CLASSES **/
......
......@@ -400,3 +400,51 @@ span.idiff {
color: $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 @@
.gfm-color_chip {
display: inline-block;
line-height: 1;
margin: 0 0 2px 4px;
vertical-align: middle;
border-radius: 3px;
......
......@@ -186,6 +186,7 @@
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
flex-wrap: nowrap;
&::-webkit-scrollbar {
display: none;
......
.modal-xl {
max-width: 98%;
}
.modal-header {
background-color: $modal-body-bg;
......
......@@ -257,6 +257,8 @@
}
.scrolling-tabs-container {
position: relative;
.merge-request-tabs-container & {
overflow: hidden;
}
......
......@@ -58,7 +58,7 @@ table {
display: none;
}
table,
&,
tbody,
td {
display: block;
......
......@@ -868,3 +868,5 @@ $input-border-color: $theme-gray-200;
$input-color: $gl-text-color;
$font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font;
$input-line-height: 20px;
$btn-line-height: 20px;
......@@ -117,6 +117,7 @@
overflow-x: scroll;
white-space: nowrap;
min-height: 200px;
display: flex;
@include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
......@@ -147,17 +148,15 @@
.board {
display: inline-block;
width: calc(85vw - 15px);
flex: 1;
min-width: 300px;
max-width: 400px;
height: 100%;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
white-space: normal;
vertical-align: top;
@include media-breakpoint-up(sm) {
width: 400px;
}
&.is-expandable {
.board-header {
cursor: pointer;
......@@ -165,6 +164,8 @@
}
&.is-collapsed {
flex: none;
min-width: 0;
width: 50px;
.board-header {
......
......@@ -189,8 +189,22 @@
img {
border: 1px solid $white-light;
background-image: linear-gradient(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-image: linear-gradient(
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-position: 0 0, 5px 5px;
max-width: 100%;
......@@ -395,6 +409,69 @@
.line_content {
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 {
......@@ -536,7 +613,7 @@
margin-right: 0;
border-color: $white-light;
cursor: pointer;
transition: all .1s ease-out;
transition: all 0.1s ease-out;
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
......@@ -563,7 +640,7 @@
height: 24px;
border-radius: 50%;
padding: 0;
transition: transform .1s ease-out;
transition: transform 0.1s ease-out;
z-index: 100;
.collapse-icon {
......@@ -708,11 +785,35 @@
width: 100%;
height: 10px;
background-color: $white-light;
background-image: linear-gradient(45deg, transparent, transparent 73%, $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-image: linear-gradient(
45deg,
transparent,
transparent 73%,
$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-repeat: repeat;
}
......@@ -750,11 +851,16 @@
.frame.click-to-comment {
position: relative;
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
cursor: -webkit-image-set(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;
cursor: -webkit-image-set(
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 {
position: absolute;
......@@ -840,7 +946,7 @@
.diff-notes-collapse,
.note,
.discussion-reply-holder, {
.discussion-reply-holder {
display: none;
}
......
......@@ -220,7 +220,7 @@
.label-link {
display: inline-flex;
vertical-align: top;
vertical-align: text-bottom;
&:hover .color-label {
text-decoration: underline;
......
......@@ -670,6 +670,7 @@
.merge-request-tabs {
display: flex;
flex-wrap: nowrap;
margin-bottom: 0;
padding: 0;
}
......
......@@ -36,6 +36,7 @@
}
.table-holder {
overflow: unset;
width: 100%;
}
......
......@@ -354,12 +354,6 @@
min-width: 200px;
}
.deploy-keys {
.scrolling-tabs-container {
position: relative;
}
}
.deploy-key {
// Ensure that the fingerprint does not overflow on small screens
.fingerprint {
......@@ -503,6 +497,12 @@
&:not(:first-child) {
border-top: 1px solid $border-color;
}
.btn-template-icon {
position: absolute;
left: $gl-padding;
top: $gl-padding;
}
}
.template-title {
......@@ -520,12 +520,6 @@
}
}
svg {
position: absolute;
left: $gl-padding;
top: $gl-padding;
}
.project-fields-form {
display: none;
......@@ -536,34 +530,23 @@
}
.template-input-group {
position: relative;
@include media-breakpoint-up(sm) {
display: flex;
}
.input-group-prepend,
.input-group-append {
.input-group-prepend {
flex: 1;
text-align: left;
padding-left: ($gl-padding * 3);
background-color: $white-light;
}
.selected-template {
line-height: 20px;
.input-group-text {
width: 100%;
background-color: $white-light;
}
.selected-icon {
padding-right: $gl-padding;
svg {
display: none;
top: 7px;
height: 20px;
width: 20px;
&.active {
display: block;
}
}
}
}
......
......@@ -335,7 +335,6 @@
img {
max-width: 90%;
max-height: 90%;
}
.isZoomable {
......@@ -553,6 +552,10 @@
}
.multi-file-commit-list-item {
&.is-active {
background-color: $white-normal;
}
.multi-file-discard-btn {
display: none;
margin-top: -2px;
......
......@@ -52,7 +52,7 @@
.settings-content {
max-height: 1px;
overflow-y: scroll;
overflow-y: hidden;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
......
......@@ -107,12 +107,12 @@
}
.performance-bar-modal {
.modal-footer {
display: none;
.modal-body {
padding: 0;
}
.modal-dialog {
width: 860px;
.modal-footer {
display: none;
}
}
}
......
......@@ -292,8 +292,10 @@ class ApplicationController < ActionController::Base
return unless current_user
return if current_user.terms_accepted?
message = _("Please accept the Terms of Service before continuing.")
if sessionless_user?
render_403
access_denied!(message)
else
# 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
......@@ -304,7 +306,7 @@ class ApplicationController < ActionController::Base
URI(request.referer).path if request.referer
end
flash[:notice] = _("Please accept the Terms of Service before continuing.")
flash[:notice] = message
redirect_to terms_path(redirect: redirect_path), status: :found
end
end
......
......@@ -24,6 +24,10 @@ module InternalRedirect
nil
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)
uri.host == request.host &&
uri.port == request.port
......
......@@ -197,15 +197,14 @@ class Projects::BlobController < Projects::ApplicationController
end
def show_json
json = blob_json(@blob)
return render_404 unless json
set_last_commit_sha
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
render json: json.merge(
json = {
id: @blob.id,
last_commit_sha: @last_commit_sha,
path: blob.path,
name: blob.name,
extension: blob.extension,
......@@ -221,6 +220,10 @@ class Projects::BlobController < Projects::ApplicationController
commits_path: project_commits_path(project, @id),
tree_path: project_tree_path(project, File.join(@ref, tree_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
......@@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder
end
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)
end
......@@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder
def feature_available_projects
# 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|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql)
table[:project_id].in(arel_query)
# 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.
"snippets.project_id IN (#{projects.to_sql})"
end
def not_project_related
table[:project_id].eq(nil)
table[:project_id].eq(nil).to_sql
end
def table
......
......@@ -173,11 +173,12 @@ module ProjectsHelper
key = [
project.route.cache_key,
project.cache_key,
project.last_activity_date,
controller.controller_name,
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"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?
......
......@@ -114,7 +114,7 @@ module CacheMarkdownField
end
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
CacheMarkdownField::CACHE_REDCARPET_VERSION
......
......@@ -72,7 +72,7 @@ class Project < ActiveRecord::Base
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
......
......@@ -43,13 +43,18 @@ class GemnasiumService < Service
def execute(data)
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(
ref: data[:ref],
before: data[:before],
after: data[:after],
token: token,
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
......@@ -562,6 +562,17 @@ module QuickActions
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)
return [] if params.nil?
......
......@@ -73,6 +73,15 @@ module ObjectStorage
upload.id)
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
def current_upload_satisfies?(paths, model)
......@@ -316,6 +325,10 @@ module ObjectStorage
super
end
def exclusive_lease_key
"object_storage_migrate:#{model.class}:#{model.id}"
end
private
def schedule_background_upload?
......@@ -382,10 +395,6 @@ module ObjectStorage
end
end
def exclusive_lease_key
"object_storage_migrate:#{model.class}:#{model.id}"
end
def with_exclusive_lease
lease_key = exclusive_lease_key
uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain
......
......@@ -12,9 +12,9 @@
- if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%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),
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-positive" }= custom_icon('emoji_smiley')
%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')
= image_tag('illustrations/error-403.svg', alt: '403', lazy: false)
......
......@@ -10,16 +10,18 @@
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview
.project-fields-form
.form-group
%label.label-light
Template
.input-group.template-input-group
.input-group-prepend
.input-group-text
.selected-icon
- Gitlab::ProjectTemplate.all.each do |template|
= custom_icon(template.logo)
.selected-template
%button.btn.btn-default.change-template{ type: "button" } Change template
.row
.form-group.col-sm-12
%label.label-light
Template
.input-group.template-input-group
.input-group-prepend
.input-group-text
.selected-icon
- Gitlab::ProjectTemplate.all.each do |template|
= custom_icon(template.logo)
.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"
......@@ -5,4 +5,4 @@
- anchors.each do |anchor|
%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
%span.stat-text= anchor.label
.stat-text= anchor.label
......@@ -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))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
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 @@
.git-empty
%fieldset
%h5 Git global setup
%pre.card.bg-light
%pre.bg-light
:preserve
git config --global user.name "#{h git_user_name}"
git config --global user.email "#{h git_user_email}"
%fieldset
%h5 Create a new repository
%pre.card.bg-light
%pre.bg-light
:preserve
git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
cd #{h @project.path}
......@@ -64,7 +64,7 @@
%fieldset
%h5 Existing folder
%pre.card.bg-light
%pre.bg-light
:preserve
cd existing_folder
git init
......@@ -77,7 +77,7 @@
%fieldset
%h5 Existing Git repository
%pre.card.bg-light
%pre.bg-light
:preserve
cd existing_repo
git remote rename origin old-origin
......
......@@ -25,7 +25,7 @@
= custom_icon ('illustration_no_commits')
- else
%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
Commits
%span.badge.badge-pill= @commits.size
......
......@@ -32,26 +32,25 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
.nav-links.scrolling-tabs.nav.nav-tabs
%ul.merge-request-tabs.nav-tabs.nav
%li.notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= tab_link_for @merge_request, :commits do
Commits
%span.badge.badge-pill= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= tab_link_for @merge_request, :pipelines do
Pipelines
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab
= tab_link_for @merge_request, :diffs do
Changes
%span.badge.badge-pill= @merge_request.diff_size
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= tab_link_for @merge_request, :commits do
Commits
%span.badge.badge-pill= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= tab_link_for @merge_request, :pipelines do
Pipelines
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab
= tab_link_for @merge_request, :diffs do
Changes
%span.badge.badge-pill= @merge_request.diff_size
- if has_vue_discussions_cookie?
#js-vue-discussion-counter
......
%pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
.build-loader-animation.js-build-refresh
......@@ -27,7 +27,7 @@
.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/form/weight", issuable: issuable, form: form
= render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
- if has_due_date
.col-lg-6
......
......@@ -6,12 +6,6 @@ class GitGarbageCollectWorker
# Timeout set to 24h
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)
project = Project.find(project_id)
active_uuid = get_lease_uuid(lease_key)
......@@ -27,21 +21,7 @@ class GitGarbageCollectWorker
end
task = task.to_sym
cmd = command(task)
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
gitaly_call(task, project.repository.raw_repository)
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
......@@ -82,21 +62,12 @@ class GitGarbageCollectWorker
when :incremental_repack
client.repack_incremental
end
end
def command(task)
case task
when :gc
git(write_bitmaps: bitmaps_enabled?) + %w[gc]
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
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
def flush_ref_caches(project)
......@@ -108,19 +79,4 @@ class GitGarbageCollectWorker
def bitmaps_enabled?
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
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
---
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