Commit 81b9b6f4 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 48960-namespace-diff-module

* master: (29 commits)
  Update the dependencies license list for 11.1.0
  Update .gitignore, .gitlab-ci.yml, and Dockerfile templates for 11.1.0
  This updates only the actual new discussion and not the full tree , which leads to a very costly full rerender
  Resolve "MR Refactor: Improve performance by setting v-once"
  Changed Inline + Parallel Views to v-if instead of v-show
  add basic export to fix timeout problem on import_file_spec.rb
  Add changelog entry for !20465
  Improve render performance of large wiki pages
  Refactor rspec matchers in read_only_spec.rb
  add CHANGELOG.md entry for !20461
  resolve node 6 compatibility issues
  Add missing foreign key in import_export_uploads
  Redesign for mr widget info and pipelines section
  Use proper markdown rendering for previews
  remove extra tick for eks docs
  Make it clear that we need to enable omniauth for SAML and Bitbucket
  Add GPL Commitment language
  Add ExclusiveLease guards for RepositoryCheck::{DispatchWorker,BatchWorker}
  Ability to check if underlying database is read only
  fix spec
  ...
parents 156a9d39 dc71b400
...@@ -39,12 +39,12 @@ export default { ...@@ -39,12 +39,12 @@ export default {
<div class="diff-viewer"> <div class="diff-viewer">
<template v-if="isTextFile"> <template v-if="isTextFile">
<inline-diff-view <inline-diff-view
v-show="isInlineView" v-if="isInlineView"
:diff-file="diffFile" :diff-file="diffFile"
:diff-lines="diffFile.highlightedDiffLines || []" :diff-lines="diffFile.highlightedDiffLines || []"
/> />
<parallel-diff-view <parallel-diff-view
v-show="isParallelView" v-if="isParallelView"
:diff-file="diffFile" :diff-file="diffFile"
:diff-lines="diffFile.parallelDiffLines || []" :diff-lines="diffFile.parallelDiffLines || []"
/> />
......
...@@ -145,6 +145,7 @@ export default { ...@@ -145,6 +145,7 @@ export default {
@click.stop="handleToggle" @click.stop="handleToggle"
/> />
<a <a
v-once
ref="titleWrapper" ref="titleWrapper"
:href="titleLink" :href="titleLink"
class="append-right-4" class="append-right-4"
......
...@@ -189,6 +189,7 @@ export default { ...@@ -189,6 +189,7 @@ export default {
</button> </button>
<a <a
v-if="lineNumber" v-if="lineNumber"
v-once
:data-linenumber="lineNumber" :data-linenumber="lineNumber"
:href="lineHref" :href="lineHref"
> >
......
...@@ -60,7 +60,7 @@ export default { ...@@ -60,7 +60,7 @@ export default {
}, },
methods: { methods: {
...mapActions('diffs', ['cancelCommentForm']), ...mapActions('diffs', ['cancelCommentForm']),
...mapActions(['saveNote', 'fetchDiscussions']), ...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm() { handleCancelCommentForm() {
this.autosave.reset(); this.autosave.reset();
this.cancelCommentForm({ this.cancelCommentForm({
...@@ -79,10 +79,10 @@ export default { ...@@ -79,10 +79,10 @@ export default {
}); });
this.saveNote(postData) this.saveNote(postData)
.then(() => { .then(result => {
const endpoint = this.getNotesDataByProp('discussionsPath'); const endpoint = this.getNotesDataByProp('discussionsPath');
this.fetchDiscussions(endpoint) this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
.then(() => { .then(() => {
this.handleCancelCommentForm(); this.handleCancelCommentForm();
}) })
......
...@@ -117,14 +117,6 @@ export default { ...@@ -117,14 +117,6 @@ export default {
<template> <template>
<td <td
v-if="isContentLine"
:class="lineType"
class="line_content"
v-html="normalizedLine.richText"
>
</td>
<td
v-else
:class="classNameMap" :class="classNameMap"
> >
<diff-line-gutter-content <diff-line-gutter-content
......
...@@ -94,11 +94,12 @@ export default { ...@@ -94,11 +94,12 @@ export default {
:is-hover="isHover" :is-hover="isHover"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<diff-table-cell <td
v-once
:class="line.type" :class="line.type"
:diff-file="diffFile" class="line_content"
:line="line" v-html="line.richText"
:is-content-line="true" >
/> </td>
</tr> </tr>
</template> </template>
...@@ -113,17 +113,15 @@ export default { ...@@ -113,17 +113,15 @@ export default {
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<diff-table-cell <td
v-once
:id="line.left.lineCode" :id="line.left.lineCode"
:diff-file="diffFile" :class="parallelViewLeftLineType"
:line="line"
:is-content-line="true"
:line-position="linePositionLeft"
:line-type="parallelViewLeftLineType"
:diff-view-type="parallelDiffViewType"
class="line_content parallel left-side" class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown" @mousedown.native="handleParallelLineMouseDown"
/> v-html="line.left.richText"
>
</td>
<diff-table-cell <diff-table-cell
:diff-file="diffFile" :diff-file="diffFile"
:line="line" :line="line"
...@@ -135,16 +133,14 @@ export default { ...@@ -135,16 +133,14 @@ export default {
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<diff-table-cell <td
v-once
:id="line.right.lineCode" :id="line.right.lineCode"
:diff-file="diffFile" :class="line.right.type"
:line="line"
:is-content-line="true"
:line-position="linePositionRight"
:line-type="line.right.type"
:diff-view-type="parallelDiffViewType"
class="line_content parallel right-side" class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown" @mousedown.native="handleParallelLineMouseDown"
/> v-html="line.right.richText"
>
</td>
</tr> </tr>
</template> </template>
...@@ -108,6 +108,11 @@ ...@@ -108,6 +108,11 @@
type: String, type: String,
required: true, required: true,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
...@@ -282,6 +287,7 @@ ...@@ -282,6 +287,7 @@
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-version="markdownVersion"
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
......
...@@ -20,6 +20,11 @@ ...@@ -20,6 +20,11 @@
type: String, type: String,
required: true, required: true,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
canAttachFile: { canAttachFile: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -47,6 +52,7 @@ ...@@ -47,6 +52,7 @@
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-version="markdownVersion"
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
> >
......
...@@ -35,6 +35,11 @@ ...@@ -35,6 +35,11 @@
type: String, type: String,
required: true, required: true,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
...@@ -97,6 +102,7 @@ ...@@ -97,6 +102,7 @@
:form-state="formState" :form-state="formState"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-version="markdownVersion"
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
......
...@@ -1251,13 +1251,15 @@ export default class Notes { ...@@ -1251,13 +1251,15 @@ export default class Notes {
var postUrl = $originalContentEl.data('postUrl'); var postUrl = $originalContentEl.data('postUrl');
var targetId = $originalContentEl.data('targetId'); var targetId = $originalContentEl.data('targetId');
var targetType = $originalContentEl.data('targetType'); var targetType = $originalContentEl.data('targetType');
var markdownVersion = $originalContentEl.data('markdownVersion');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM); this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm $editForm
.find('form') .find('form')
.attr('action', `${postUrl}?html=true`) .attr('action', `${postUrl}?html=true`)
.attr('data-remote', 'true'); .attr('data-remote', 'true')
.attr('data-markdown-version', markdownVersion);
$editForm.find('.js-form-target-id').val(targetId); $editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType); $editForm.find('.js-form-target-type').val(targetType);
$editForm $editForm
......
...@@ -34,6 +34,11 @@ export default { ...@@ -34,6 +34,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
}, },
data() { data() {
return { return {
...@@ -344,6 +349,7 @@ Please check your network connection and try again.`; ...@@ -344,6 +349,7 @@ Please check your network connection and try again.`;
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath" :quick-actions-docs-path="quickActionsDocsPath"
:markdown-version="markdownVersion"
:add-spacing-classes="false"> :add-spacing-classes="false">
<textarea <textarea
id="note-body" id="note-body"
......
...@@ -92,6 +92,7 @@ export default { ...@@ -92,6 +92,7 @@ export default {
:is-editing="isEditing" :is-editing="isEditing"
:note-body="noteBody" :note-body="noteBody"
:note-id="note.id" :note-id="note.id"
:markdown-version="note.cached_markdown_version"
@handleFormUpdate="handleFormUpdate" @handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler" @cancelForm="formCancelHandler"
/> />
......
...@@ -24,6 +24,11 @@ export default { ...@@ -24,6 +24,11 @@ export default {
required: false, required: false,
default: 0, default: 0,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
saveButtonTitle: { saveButtonTitle: {
type: String, type: String,
required: false, required: false,
...@@ -156,6 +161,7 @@ export default { ...@@ -156,6 +161,7 @@ export default {
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath" :quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"> :add-spacing-classes="false">
<textarea <textarea
......
...@@ -43,6 +43,11 @@ export default { ...@@ -43,6 +43,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
}, },
data() { data() {
return { return {
...@@ -192,6 +197,7 @@ export default { ...@@ -192,6 +197,7 @@ export default {
<comment-form <comment-form
:noteable-type="noteableType" :noteable-type="noteableType"
:markdown-version="markdownVersion"
/> />
</div> </div>
</template> </template>
...@@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData); const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData); const noteableData = JSON.parse(notesDataset.noteableData);
const { markdownVersion } = notesDataset;
let currentUserData = {}; let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType; noteableData.noteableType = notesDataset.noteableType;
...@@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
return { return {
noteableData, noteableData,
currentUserData, currentUserData,
markdownVersion,
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
}, },
...@@ -42,6 +44,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -42,6 +44,7 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData: this.noteableData, noteableData: this.noteableData,
notesData: this.notesData, notesData: this.notesData,
userData: this.currentUserData, userData: this.currentUserData,
markdownVersion: this.markdownVersion,
}, },
}); });
}, },
......
...@@ -41,6 +41,15 @@ export const fetchDiscussions = ({ commit }, path) => ...@@ -41,6 +41,15 @@ export const fetchDiscussions = ({ commit }, path) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions); commit(types.SET_INITIAL_DISCUSSIONS, discussions);
}); });
export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
service
.fetchDiscussions(path)
.then(res => res.json())
.then(discussions => {
const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
});
export const deleteNote = ({ commit }, note) => export const deleteNote = ({ commit }, note) =>
service.deleteNote(note.path).then(() => { service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note); commit(types.DELETE_NOTE, note);
......
...@@ -114,7 +114,6 @@ export default { ...@@ -114,7 +114,6 @@ export default {
Object.assign(state, { discussions }); Object.assign(state, { discussions });
}, },
[types.SET_LAST_FETCHED_AT](state, fetchedAt) { [types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt }); Object.assign(state, { lastFetchedAt: fetchedAt });
}, },
......
...@@ -28,12 +28,16 @@ MarkdownPreview.prototype.ajaxCache = {}; ...@@ -28,12 +28,16 @@ MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) { MarkdownPreview.prototype.showPreview = function ($form) {
var mdText; var mdText;
var markdownVersion;
var url;
var preview = $form.find('.js-md-preview'); var preview = $form.find('.js-md-preview');
var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) { if (preview.hasClass('md-preview-loading')) {
return; return;
} }
mdText = $form.find('textarea.markdown-area').val(); mdText = $form.find('textarea.markdown-area').val();
markdownVersion = $form.attr('data-markdown-version');
url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
if (mdText.trim().length === 0) { if (mdText.trim().length === 0) {
preview.text(this.emptyMessage); preview.text(this.emptyMessage);
...@@ -59,6 +63,14 @@ MarkdownPreview.prototype.showPreview = function ($form) { ...@@ -59,6 +63,14 @@ MarkdownPreview.prototype.showPreview = function ($form) {
} }
}; };
MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) {
if (typeof markdownVersion === 'undefined') {
return markdownPreviewPath;
}
return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`;
};
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!url) { if (!url) {
return; return;
......
...@@ -79,66 +79,62 @@ export default { ...@@ -79,66 +79,62 @@ export default {
</script> </script>
<template> <template>
<div class="mr-widget-heading deploy-heading"> <div class="mr-widget-heading deploy-heading append-bottom-default">
<div class="ci-widget media"> <div class="ci-widget media">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
<status-icon status="success" />
</span>
</div>
<div class="media-body"> <div class="media-body">
<div class="deploy-body"> <div class="deploy-body">
<template v-if="hasDeploymentMeta"> <div class="deployment-info">
<span> <template v-if="hasDeploymentMeta">
Deployed to <span>
</span> Deployed to
<a </span>
:href="deployment.url" <a
target="_blank" :href="deployment.url"
rel="noopener noreferrer nofollow" target="_blank"
class="deploy-link js-deploy-meta" rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-meta"
>
{{ deployment.name }}
</a>
</template>
<span
v-tooltip
v-if="hasDeploymentTime"
:title="deployment.deployed_at_formatted"
class="js-deploy-time"
> >
{{ deployment.name }} {{ deployTimeago }}
</a>
</template>
<template v-if="hasExternalUrls">
<span>
on
</span> </span>
<memory-usage
v-if="hasMetrics"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div>
<div>
<a <a
v-if="hasExternalUrls"
:href="deployment.external_url" :href="deployment.external_url"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-url" class="deploy-link js-deploy-url btn btn-default btn-sm inline"
> >
{{ deployment.external_url_formatted }} <span>
<icon View app
:size="16" <icon name="external-link" />
name="external-link" </span>
/>
</a> </a>
</template> <loading-button
<span v-if="deployment.stop_url"
v-tooltip :loading="isStopping"
v-if="hasDeploymentTime" container-class="btn btn-default btn-sm inline prepend-left-4"
:title="deployment.deployed_at_formatted" title="Stop environment"
class="js-deploy-time" @click="stopEnvironment"
> >
{{ deployTimeago }} <icon name="stop" />
</span> </loading-button>
<loading-button </div>
v-if="deployment.stop_url"
:loading="isStopping"
container-class="btn btn-default btn-sm prepend-left-default"
label="Stop environment"
@click="stopEnvironment"
/>
</div> </div>
<memory-usage
v-if="hasMetrics"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility'; import { webIDEUrl } from '~/lib/utils/url_utility';
import icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
...@@ -11,7 +11,7 @@ export default { ...@@ -11,7 +11,7 @@ export default {
tooltip, tooltip,
}, },
components: { components: {
icon, Icon,
clipboardButton, clipboardButton,
}, },
props: { props: {
...@@ -54,104 +54,114 @@ export default { ...@@ -54,104 +54,114 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="mr-source-target"> <div class="mr-source-target append-bottom-default">
<div class="normal"> <div class="git-merge-icon-container append-right-default">
<strong> <icon name="git-merge" />
{{ s__("mrWidget|Request to merge") }} </div>
<span <div class="git-merge-container d-flex">
:class="{ 'label-truncated': isSourceBranchLong }" <div class="normal">
:title="isSourceBranchLong ? mr.sourceBranch : ''" <strong>
:v-tooltip="isSourceBranchLong" {{ s__("mrWidget|Request to merge") }}
class="label-branch js-source-branch" <span
data-placement="bottom" :class="{ 'label-truncated': isSourceBranchLong }"
v-html="mr.sourceBranchLink" :title="isSourceBranchLong ? mr.sourceBranch : ''"
> :v-tooltip="isSourceBranchLong"
</span> class="label-branch js-source-branch"
data-placement="bottom"
v-html="mr.sourceBranchLink"
>
</span>
<clipboard-button <clipboard-button
:text="branchNameClipboardData" :text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')" :title="__('Copy branch name to clipboard')"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
{{ s__("mrWidget|into") }} {{ s__("mrWidget|into") }}
<span <span
:v-tooltip="isTargetBranchLong" :v-tooltip="isTargetBranchLong"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }" :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
:title="isTargetBranchLong ? mr.targetBranch : ''" :title="isTargetBranchLong ? mr.targetBranch : ''"
class="label-branch" class="label-branch"
data-placement="bottom" data-placement="bottom"
>
<a
:href="mr.targetBranchTreePath"
class="js-target-branch"
> >
{{ mr.targetBranch }} <a
</a> :href="mr.targetBranchTreePath"
</span> class="js-target-branch"
</strong> >
<span {{ mr.targetBranch }}
v-if="shouldShowCommitsBehindText" </a>
class="diverged-commits-count" </span>
> </strong>
(<a :href="mr.targetBranchPath">{{ commitsText }}</a>) <div
</span> v-if="shouldShowCommitsBehindText"
</div> class="diverged-commits-count"
>
<span class="monospace">{{ mr.sourceBranch }}</span>
is {{ commitsText }}
<span class="monospace">{{ mr.targetBranch }}</span>
</div>
</div>
<div v-if="mr.isOpen"> <div
<a v-if="mr.isOpen"
v-if="!mr.sourceBranchRemoved" class="branch-actions"
:href="webIdePath"
class="btn btn-sm btn-default inline js-web-ide"
>
{{ s__("mrWidget|Web IDE") }}
</a>
<button
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
class="btn btn-sm btn-default inline js-check-out-branch"
type="button"
> >
{{ s__("mrWidget|Check out branch") }} <a
</button> v-if="!mr.sourceBranchRemoved"
<span class="dropdown prepend-left-10"> :href="webIdePath"
class="btn btn-default inline js-web-ide d-none d-md-inline-block"
>
{{ s__("mrWidget|Open in Web IDE") }}
</a>
<button <button
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
class="btn btn-default inline js-check-out-branch"
type="button" type="button"
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
aria-haspopup="true"
aria-expanded="false"
> >
<icon name="download" /> {{ s__("mrWidget|Check out branch") }}
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-right"> <span class="dropdown prepend-left-10">
<li> <button
<a type="button"
:href="mr.emailPatchesPath" class="btn inline dropdown-toggle"
class="js-download-email-patches" data-toggle="dropdown"
download aria-label="Download as"
> aria-haspopup="true"
{{ s__("mrWidget|Email patches") }} aria-expanded="false"
</a> >
</li> <icon name="download" />
<li> <i
<a class="fa fa-caret-down"
:href="mr.plainDiffPath" aria-hidden="true">
class="js-download-plain-diff" </i>
download </button>
> <ul class="dropdown-menu dropdown-menu-right">
{{ s__("mrWidget|Plain diff") }} <li>
</a> <a
</li> :href="mr.emailPatchesPath"
</ul> class="js-download-email-patches"
</span> download
>
{{ s__("mrWidget|Email patches") }}
</a>
</li>
<li>
<a
:href="mr.plainDiffPath"
class="js-download-plain-diff"
download
>
{{ s__("mrWidget|Plain diff") }}
</a>
</li>
</ul>
</span>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -26,6 +26,10 @@ export default { ...@@ -26,6 +26,10 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
sourceBranchLink: {
type: String,
required: false,
},
}, },
computed: { computed: {
hasPipeline() { hasPipeline() {
...@@ -54,12 +58,18 @@ export default { ...@@ -54,12 +58,18 @@ export default {
<template> <template>
<div <div
v-if="hasPipeline || hasCIError" v-if="hasPipeline || hasCIError"
class="mr-widget-heading" class="mr-widget-heading append-bottom-default"
> >
<div class="ci-widget media"> <div class="ci-widget media">
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <div
<icon name="status_failed" /> class="add-border ci-status-icon ci-status-icon-failed ci-error
js-ci-error append-right-default"
>
<icon
:size="32"
name="status_failed_borderless"
/>
</div> </div>
<div class="media-body"> <div class="media-body">
Could not connect to the CI server. Please check your settings and try again Could not connect to the CI server. Please check your settings and try again
...@@ -68,50 +78,66 @@ export default { ...@@ -68,50 +78,66 @@ export default {
<template v-else-if="hasPipeline"> <template v-else-if="hasPipeline">
<a <a
:href="status.details_path" :href="status.details_path"
class="append-right-10" class="align-self-start append-right-default"
> >
<ci-icon :status="status" /> <ci-icon
:status="status"
:size="32"
:borderless="true"
class="add-border"
/>
</a> </a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
<div class="font-weight-bold">
Pipeline
<a
:href="pipeline.path"
class="pipeline-id font-weight-normal pipeline-number"
>#{{ pipeline.id }}</a>
<div class="media-body"> {{ pipeline.details.status.label }}
Pipeline
<a
:href="pipeline.path"
class="pipeline-id"
>
#{{ pipeline.id }}
</a>
{{ pipeline.details.status.label }}
<template v-if="hasCommitInfo"> <template v-if="hasCommitInfo">
for for
<a
<a :href="pipeline.commit.commit_path"
:href="pipeline.commit.commit_path" class="commit-sha js-commit-link font-weight-normal"
class="commit-sha js-commit-link" >
> {{ pipeline.commit.short_id }}</a>
{{ pipeline.commit.short_id }}</a>. on
</template> <span
class="label-branch"
<span class="mr-widget-pipeline-graph"> v-html="sourceBranchLink"
<span >
v-if="hasStages" </span>
class="stage-cell" </template>
> </div>
<div <div
v-for="(stage, i) in pipeline.details.stages" v-if="pipeline.coverage"
:key="i" class="coverage"
class="stage-container dropdown js-mini-pipeline-graph"
> >
<pipeline-stage :stage="stage" /> Coverage {{ pipeline.coverage }}%
</div> </div>
</div>
</div>
<div>
<span class="mr-widget-pipeline-graph">
<span
v-if="hasStages"
class="stage-cell"
>
<div
v-for="(stage, i) in pipeline.details.stages"
:key="i"
class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
>
<pipeline-stage :stage="stage" />
</div>
</span>
</span> </span>
</span> </div>
<template v-if="pipeline.coverage">
Coverage {{ pipeline.coverage }}%
</template>
</div> </div>
</template> </template>
</div> </div>
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
<ci-icon <ci-icon
v-else v-else
:status="statusObj" :status="statusObj"
:size="24"
/> />
<button <button
......
...@@ -252,41 +252,44 @@ export default { ...@@ -252,41 +252,44 @@ export default {
:pipeline="mr.pipeline" :pipeline="mr.pipeline"
:ci-status="mr.ciStatus" :ci-status="mr.ciStatus"
:has-ci="mr.hasCI" :has-ci="mr.hasCI"
:source-branch-link="mr.sourceBranchLink"
/> />
<deployment <deployment
v-for="deployment in mr.deployments" v-for="deployment in mr.deployments"
:key="deployment.id" :key="deployment.id"
:deployment="deployment" :deployment="deployment"
/> />
<div class="mr-widget-section"> <div class="mr-section-container">
<component <div class="mr-widget-section">
:is="componentName" <component
:mr="mr" :is="componentName"
:service="service" :mr="mr"
/> :service="service"
/>
<section <section
v-if="mr.allowCollaboration" v-if="mr.allowCollaboration"
class="mr-info-list mr-links" class="mr-info-list mr-links"
> >
{{ s__("mrWidget|Allows commits from members who can merge to the target branch") }} {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
</section> </section>
<mr-widget-related-links <mr-widget-related-links
v-if="shouldRenderRelatedLinks" v-if="shouldRenderRelatedLinks"
:state="mr.state" :state="mr.state"
:related-links="mr.relatedLinks" :related-links="mr.relatedLinks"
/> />
<source-branch-removal-status <source-branch-removal-status
v-if="shouldRenderSourceBranchRemovalStatus" v-if="shouldRenderSourceBranchRemovalStatus"
/> />
</div> </div>
<div <div
v-if="shouldRenderMergeHelp" v-if="shouldRenderMergeHelp"
class="mr-widget-footer" class="mr-widget-footer"
> >
<mr-widget-merge-help /> <mr-widget-merge-help />
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { s__ } from '~/locale';
import Flash from '../../../flash'; import Flash from '../../../flash';
import GLForm from '../../../gl_form'; import GLForm from '../../../gl_form';
import markdownHeader from './header.vue'; import markdownHeader from './header.vue';
...@@ -22,6 +23,11 @@ ...@@ -22,6 +23,11 @@
type: String, type: String,
required: true, required: true,
}, },
markdownVersion: {
type: Number,
required: false,
default: 0,
},
addSpacingClasses: { addSpacingClasses: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -92,10 +98,11 @@ ...@@ -92,10 +98,11 @@
if (text) { if (text) {
this.markdownPreviewLoading = true; this.markdownPreviewLoading = true;
this.$http.post(this.markdownPreviewPath, { text }) this.$http
.then(resp => resp.json()) .post(this.versionedPreviewPath(), { text })
.then(data => this.renderMarkdown(data)) .then(resp => resp.json())
.catch(() => new Flash('Error loading markdown preview')); .then(data => this.renderMarkdown(data))
.catch(() => new Flash(s__('Error loading markdown preview')));
} else { } else {
this.renderMarkdown(); this.renderMarkdown();
} }
...@@ -119,6 +126,13 @@ ...@@ -119,6 +126,13 @@
$(this.$refs['markdown-preview']).renderGFM(); $(this.$refs['markdown-preview']).renderGFM();
}); });
}, },
versionedPreviewPath() {
const { markdownPreviewPath, markdownVersion } = this;
return `${markdownPreviewPath}${
markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
}markdown_version=${markdownVersion}`;
},
}, },
}; };
</script> </script>
......
...@@ -3,12 +3,20 @@ ...@@ -3,12 +3,20 @@
svg { svg {
fill: $green-500; fill: $green-500;
} }
&.add-border {
@include borderless-status-icon($green-500);
}
} }
.ci-status-icon-failed { .ci-status-icon-failed {
svg { svg {
fill: $gl-danger; fill: $gl-danger;
} }
&.add-border {
@include borderless-status-icon($red-500);
}
} }
.ci-status-icon-pending, .ci-status-icon-pending,
...@@ -17,12 +25,20 @@ ...@@ -17,12 +25,20 @@
svg { svg {
fill: $orange-500; fill: $orange-500;
} }
&.add-border {
@include borderless-status-icon($orange-500);
}
} }
.ci-status-icon-running { .ci-status-icon-running {
svg { svg {
fill: $blue-400; fill: $blue-400;
} }
&.add-border {
@include borderless-status-icon($blue-400);
}
} }
.ci-status-icon-canceled, .ci-status-icon-canceled,
...@@ -30,6 +46,10 @@ ...@@ -30,6 +46,10 @@
svg { svg {
fill: $gl-text-color; fill: $gl-text-color;
} }
&.add-border {
@include borderless-status-icon($gl-text-color);
}
} }
.ci-status-icon-created, .ci-status-icon-created,
...@@ -38,6 +58,10 @@ ...@@ -38,6 +58,10 @@
svg { svg {
fill: $gray-darkest; fill: $gray-darkest;
} }
&.add-border {
@include borderless-status-icon($gray-darkest);
}
} }
.ci-status-icon-manual { .ci-status-icon-manual {
......
...@@ -232,3 +232,10 @@ ...@@ -232,3 +232,10 @@
word-break: break-word; word-break: break-word;
max-width: 100%; max-width: 100%;
} }
@mixin borderless-status-icon($color) {
svg {
border: 1px solid $color;
border-radius: 50%;
}
}
...@@ -350,7 +350,8 @@ code { ...@@ -350,7 +350,8 @@ code {
} }
.commit-sha, .commit-sha,
.ref-name { .ref-name,
.pipeline-number {
@extend .monospace; @extend .monospace;
font-size: 95%; font-size: 95%;
} }
......
...@@ -743,6 +743,7 @@ Pipeline Graph ...@@ -743,6 +743,7 @@ Pipeline Graph
*/ */
$stage-hover-bg: $gray-darker; $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px; $ci-action-icon-size: 22px;
$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px; $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px; $pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px; $ci-action-dropdown-button-size: 24px;
......
...@@ -15,16 +15,38 @@ ...@@ -15,16 +15,38 @@
} }
} }
.mr-widget-heading {
position: relative;
border: 1px solid $border-color;
border-radius: 4px;
&:not(.deploy-heading)::before {
content: '';
border-left: 1px solid $theme-gray-200;
position: absolute;
left: 32px;
top: -17px;
height: 16px;
}
}
.mr-section-container {
border: 1px solid $border-color;
border-radius: $border-radius-default;
border-top: 0;
}
.mr-widget-heading,
.mr-widget-section,
.mr-widget-footer {
padding: $gl-padding;
}
.mr-state-widget { .mr-state-widget {
color: $gl-text-color; color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 2px;
line-height: 28px;
.mr-widget-heading,
.mr-widget-section, .mr-widget-section,
.mr-widget-footer { .mr-widget-footer {
padding: $gl-padding;
border-top: solid 1px $border-color; border-top: solid 1px $border-color;
} }
...@@ -124,10 +146,17 @@ ...@@ -124,10 +146,17 @@
.ci-widget { .ci-widget {
color: $gl-text-color; color: $gl-text-color;
display: flex; display: flex;
align-items: center;
justify-content: space-between;
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
flex-wrap: wrap; flex-wrap: wrap;
} }
.ci-widget-content {
display: flex;
align-items: center;
}
} }
.mr-widget-icon { .mr-widget-icon {
...@@ -136,8 +165,6 @@ ...@@ -136,8 +165,6 @@
} }
.ci-status-icon svg { .ci-status-icon svg {
width: $status-icon-size;
height: $status-icon-size;
margin: 3px 0; margin: 3px 0;
position: relative; position: relative;
overflow: visible; overflow: visible;
...@@ -145,8 +172,6 @@ ...@@ -145,8 +172,6 @@
} }
.mr-widget-pipeline-graph { .mr-widget-pipeline-graph {
padding: 0 4px;
.dropdown-menu { .dropdown-menu {
z-index: 300; z-index: 300;
} }
...@@ -157,7 +182,7 @@ ...@@ -157,7 +182,7 @@
} }
.normal { .normal {
line-height: 28px; flex: 1;
} }
.capitalize { .capitalize {
...@@ -168,7 +193,7 @@ ...@@ -168,7 +193,7 @@
@extend .ref-name; @extend .ref-name;
color: $gl-text-color; color: $gl-text-color;
font-weight: $gl-font-weight-bold; font-weight: normal;
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
...@@ -192,6 +217,8 @@ ...@@ -192,6 +217,8 @@
} }
.mr-widget-body { .mr-widget-body {
line-height: 28px;
@include clearfix; @include clearfix;
&.media > *:first-child { &.media > *:first-child {
...@@ -474,18 +501,66 @@ ...@@ -474,18 +501,66 @@
} }
} }
.merge-request-details .content-block {
border-bottom: 0;
}
.mr-source-target { .mr-source-target {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; border-radius: $border-radius-default;
align-items: center; padding: $gl-padding;
background-color: $gray-light; border: 1px solid $border-color;
border-radius: $border-radius-default $border-radius-default 0 0; min-height: 69px;
padding: $gl-padding / 2 $gl-padding;
@include media-breakpoint-up(md) {
align-items: center;
}
.dropdown-toggle .fa { .dropdown-toggle .fa {
color: $gl-text-color; color: $gl-text-color;
} }
.git-merge-icon-container {
border: 1px solid $theme-gray-400;
border-radius: 50%;
height: 32px;
width: 32px;
color: $theme-gray-700;
line-height: 28px;
.ic-git-merge {
vertical-align: middle;
width: 31px;
}
}
.git-merge-container {
justify-content: space-between;
flex: 1;
flex-direction: row;
align-items: center;
@include media-breakpoint-down(md) {
flex-direction: column;
align-items: flex-start;
.branch-actions {
margin-top: 16px;
}
}
@include media-breakpoint-up(lg) {
.branch-actions {
align-self: center;
}
}
}
.diverged-commits-count {
color: $gl-text-color-secondary;
font-size: 12px;
}
} }
.card-new-merge-request { .card-new-merge-request {
...@@ -720,13 +795,25 @@ ...@@ -720,13 +795,25 @@
} }
.deploy-heading { .deploy-heading {
margin-top: -19px;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-color: $gray-light;
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
.media-body { .media-body {
min-width: 0; min-width: 0;
font-size: 12px;
margin-left: 48px;
} }
} }
.deploy-body { .deploy-body {
display: flex; display: flex;
align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
@include media-breakpoint-up(xs) { @include media-breakpoint-up(xs) {
...@@ -734,6 +821,15 @@ ...@@ -734,6 +821,15 @@
white-space: nowrap; white-space: nowrap;
} }
@include media-breakpoint-down(md) {
flex-direction: column;
align-items: flex-start;
.deployment-info {
margin-bottom: $gl-padding;
}
}
> *:not(:last-child) { > *:not(:last-child) {
margin-right: .3em; margin-right: .3em;
} }
...@@ -741,18 +837,22 @@ ...@@ -741,18 +837,22 @@
svg { svg {
vertical-align: text-top; vertical-align: text-top;
} }
}
.deploy-link { .deployment-info {
white-space: nowrap; flex: 1;
overflow: hidden; white-space: nowrap;
text-overflow: ellipsis; overflow: hidden;
min-width: 100px; text-overflow: ellipsis;
max-width: 150px; min-width: 100px;
@include media-breakpoint-up(xs) { @include media-breakpoint-up(xs) {
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
}
}
.btn svg {
fill: $theme-gray-700;
} }
} }
...@@ -772,3 +872,33 @@ ...@@ -772,3 +872,33 @@
} }
} }
} }
.ci-widget-container {
justify-content: space-between;
flex: 1;
flex-direction: row;
@include media-breakpoint-down(md) {
flex-direction: column;
.stage-cell .stage-container {
margin-top: 16px;
}
.dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
transform: initial;
}
}
.coverage {
font-size: 12px;
color: $theme-gray-700;
line-height: initial;
}
.mini-pipeline-graph-dropdown-toggle,
.stage-cell .mini-pipeline-graph-dropdown-toggle svg {
height: $ci-action-icon-size-lg;
width: $ci-action-icon-size-lg;
}
}
...@@ -301,6 +301,21 @@ ...@@ -301,6 +301,21 @@
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
} }
} }
//delete when all pipelines are updated to new size
&.mr-widget-pipeline-stages {
+ .stage-container {
margin-left: 4px;
}
&:not(:last-child) {
&::after {
width: 4px;
right: -4px;
top: 11px;
}
}
}
} }
} }
......
...@@ -14,6 +14,8 @@ module PreviewMarkdown ...@@ -14,6 +14,8 @@ module PreviewMarkdown
else {} else {}
end end
markdown_params[:markdown_engine] = result[:markdown_engine]
render json: { render json: {
body: view_context.markdown(result[:text], markdown_params), body: view_context.markdown(result[:text], markdown_params),
references: { references: {
......
...@@ -2,6 +2,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include PreviewMarkdown include PreviewMarkdown
include SendFileUpload
before_action :whitelist_query_limiting, only: [:create] before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
...@@ -188,9 +189,9 @@ class ProjectsController < Projects::ApplicationController ...@@ -188,9 +189,9 @@ class ProjectsController < Projects::ApplicationController
end end
def download_export def download_export
export_project_path = @project.export_project_path if export_project_object_storage?
send_upload(@project.import_export_upload.export_file)
if export_project_path elsif export_project_path
send_file export_project_path, disposition: 'attachment' send_file export_project_path, disposition: 'attachment'
else else
redirect_to( redirect_to(
...@@ -265,8 +266,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -265,8 +266,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json render json: options.to_json
end end
private
# Render project landing depending of which features are available # Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page # So if page is not availble in the list it renders the next page
# #
...@@ -424,4 +423,12 @@ class ProjectsController < Projects::ApplicationController ...@@ -424,4 +423,12 @@ class ProjectsController < Projects::ApplicationController
def whitelist_query_limiting def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end end
def export_project_path
@export_project_path ||= @project.export_project_path
end
def export_project_object_storage?
@project.export_project_object_exists?
end
end end
...@@ -249,6 +249,7 @@ module IssuablesHelper ...@@ -249,6 +249,7 @@ module IssuablesHelper
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(parent), markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
markdownVersion: issuable.cached_markdown_version,
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
......
...@@ -107,6 +107,7 @@ module MarkupHelper ...@@ -107,6 +107,7 @@ module MarkupHelper
def markup(file_name, text, context = {}) def markup(file_name, text, context = {})
context[:project] ||= @project context[:project] ||= @project
context[:markdown_engine] ||= :redcarpet
html = context.delete(:rendered) || markup_unsafe(file_name, text, context) html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
...@@ -120,7 +121,8 @@ module MarkupHelper ...@@ -120,7 +121,8 @@ module MarkupHelper
project: @project, project: @project,
project_wiki: @project_wiki, project_wiki: @project_wiki,
page_slug: wiki_page.slug, page_slug: wiki_page.slug,
issuable_state_filter_enabled: true issuable_state_filter_enabled: true,
markdown_engine: :redcarpet
} }
html = html =
......
...@@ -169,6 +169,7 @@ module NotesHelper ...@@ -169,6 +169,7 @@ module NotesHelper
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'), newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
markdownVersion: issuable.cached_markdown_version,
quickActionsDocsPath: help_page_path('user/project/quick_actions'), quickActionsDocsPath: help_page_path('user/project/quick_actions'),
closePath: close_issuable_path(issuable), closePath: close_issuable_path(issuable),
reopenPath: reopen_issuable_path(issuable), reopenPath: reopen_issuable_path(issuable),
......
...@@ -153,7 +153,7 @@ class NotifyPreview < ActionMailer::Preview ...@@ -153,7 +153,7 @@ class NotifyPreview < ActionMailer::Preview
cleanup do cleanup do
note = yield note = yield
Notify.public_send(method, user.id, note) Notify.public_send(method, user.id, note) # rubocop:disable GitlabSecurity/PublicSend
end end
end end
......
...@@ -40,6 +40,18 @@ module CacheMarkdownField ...@@ -40,6 +40,18 @@ module CacheMarkdownField
end end
end end
class MarkdownEngine
def self.from_version(version = nil)
return :common_mark if version.nil? || version == 0
if version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
:redcarpet
else
:common_mark
end
end
end
def skip_project_check? def skip_project_check?
false false
end end
...@@ -57,7 +69,7 @@ module CacheMarkdownField ...@@ -57,7 +69,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key # Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author) context[:author] = self.author if self.respond_to?(:author)
context[:markdown_engine] = markdown_engine context[:markdown_engine] = MarkdownEngine.from_version(latest_cached_markdown_version)
context context
end end
...@@ -123,14 +135,6 @@ module CacheMarkdownField ...@@ -123,14 +135,6 @@ module CacheMarkdownField
end end
end end
def markdown_engine
if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
:redcarpet
else
:common_mark
end
end
included do included do
cattr_reader :cached_markdown_fields do cattr_reader :cached_markdown_fields do
FieldData.new FieldData.new
......
...@@ -7,7 +7,7 @@ module CacheableAttributes ...@@ -7,7 +7,7 @@ module CacheableAttributes
class_methods do class_methods do
def cache_key def cache_key
"#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}".freeze "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze
end end
# Can be overriden # Can be overriden
...@@ -69,6 +69,6 @@ module CacheableAttributes ...@@ -69,6 +69,6 @@ module CacheableAttributes
end end
def cache! def cache!
Rails.cache.write(self.class.cache_key, self) Rails.cache.write(self.class.cache_key, self, expires_in: 1.minute)
end end
end end
class ImportExportUpload < ActiveRecord::Base
include WithUploads
include ObjectStorage::BackgroundMove
belongs_to :project
mount_uploader :import_file, ImportExportUploader
mount_uploader :export_file, ImportExportUploader
def retrieve_upload(_identifier, paths)
Upload.find_by(model: self, path: paths)
end
end
...@@ -171,6 +171,7 @@ class Project < ActiveRecord::Base ...@@ -171,6 +171,7 @@ class Project < ActiveRecord::Base
has_one :fork_network, through: :fork_network_member has_one :fork_network, through: :fork_network_member
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id' has_many :merge_requests, foreign_key: 'target_project_id'
...@@ -1712,7 +1713,7 @@ class Project < ActiveRecord::Base ...@@ -1712,7 +1713,7 @@ class Project < ActiveRecord::Base
:started :started
elsif after_export_in_progress? elsif after_export_in_progress?
:after_export_action :after_export_action
elsif export_project_path elsif export_project_path || export_project_object_exists?
:finished :finished
else else
:none :none
...@@ -1727,16 +1728,21 @@ class Project < ActiveRecord::Base ...@@ -1727,16 +1728,21 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress? import_export_shared.after_export_in_progress?
end end
def remove_exports def remove_exports(path = export_path)
return nil unless export_path.present? if path.present?
FileUtils.rm_rf(path)
FileUtils.rm_rf(export_path) elsif export_project_object_exists?
import_export_upload.remove_export_file!
import_export_upload.save
end
end end
def remove_exported_project_file def remove_exported_project_file
return unless export_project_path.present? remove_exports(export_project_path)
end
FileUtils.rm_f(export_project_path) def export_project_object_exists?
Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
end end
def full_path_slug def full_path_slug
......
...@@ -83,7 +83,7 @@ class Repository ...@@ -83,7 +83,7 @@ class Repository
@raw_repository&.cleanup @raw_repository&.cleanup
end end
# Return absolute path to repository # Don't use this! It's going away. Use Gitaly to read or write from repos.
def path_to_repo def path_to_repo
@path_to_repo ||= @path_to_repo ||=
begin begin
...@@ -250,7 +250,7 @@ class Repository ...@@ -250,7 +250,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes) # This will still fail if the file is corrupted (e.g. 0 bytes)
raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
rescue Gitlab::Git::CommandError => ex rescue Gitlab::Git::CommandError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end end
def kept_around?(sha) def kept_around?(sha)
...@@ -564,7 +564,7 @@ class Repository ...@@ -564,7 +564,7 @@ class Repository
end end
def rendered_readme def rendered_readme
MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme
end end
cache_method :rendered_readme cache_method :rendered_readme
......
...@@ -62,6 +62,8 @@ class NoteEntity < API::Entities::Note ...@@ -62,6 +62,8 @@ class NoteEntity < API::Entities::Note
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? } expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
expose :cached_markdown_version
private private
def current_user def current_user
......
...@@ -10,7 +10,9 @@ class ImportExportCleanUpService ...@@ -10,7 +10,9 @@ class ImportExportCleanUpService
def execute def execute
Gitlab::Metrics.measure(:import_export_clean_up) do Gitlab::Metrics.measure(:import_export_clean_up) do
next unless File.directory?(path) clean_up_export_object_files
break unless File.directory?(path)
clean_up_export_files clean_up_export_files
end end
...@@ -21,4 +23,11 @@ class ImportExportCleanUpService ...@@ -21,4 +23,11 @@ class ImportExportCleanUpService
def clean_up_export_files def clean_up_export_files
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete)) Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end end
def clean_up_export_object_files
ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload|
upload.remove_export_file!
upload.save!
end
end
end end
...@@ -6,7 +6,8 @@ class PreviewMarkdownService < BaseService ...@@ -6,7 +6,8 @@ class PreviewMarkdownService < BaseService
success( success(
text: text, text: text,
users: users, users: users,
commands: commands.join(' ') commands: commands.join(' '),
markdown_engine: markdown_engine
) )
end end
...@@ -42,4 +43,8 @@ class PreviewMarkdownService < BaseService ...@@ -42,4 +43,8 @@ class PreviewMarkdownService < BaseService
def commands_target_id def commands_target_id
params[:quick_actions_target_id] params[:quick_actions_target_id]
end end
def markdown_engine
CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
end
end end
class ImportExportUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[tar.gz].freeze
def extension_whitelist
EXTENSION_WHITELIST
end
def move_to_store
true
end
def move_to_cache
false
end
end
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%li Any encrypted tokens %li Any encrypted tokens
%p %p
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page. Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- if project.export_project_path - if project.export_status == :finished
= link_to 'Download export', download_export_project_path(project), = link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default" rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project), = link_to 'Generate new export', generate_new_export_project_path(project),
......
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @issue],
html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' },
data: { markdown_version: @issue.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue = render 'shared/issuable/form', f: f, issuable: @issue
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' },
data: { markdown_version: @merge_request.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request = render 'shared/issuable/form', f: f, issuable: @merge_request
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, @milestone],
html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'},
data: { markdown_version: @milestone.cached_markdown_version } do |f|
= form_errors(@milestone) = form_errors(@milestone)
.row .row
.col-md-6 .col-md-6
......
...@@ -11,7 +11,9 @@ ...@@ -11,7 +11,9 @@
%strong= @tag.name %strong= @tag.name
= form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f| = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name),
html: { class: 'common-note-form release-form js-quick-submit' },
data: { markdown_version: @release.cached_markdown_version }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…" = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints' = render 'shared/notes/hints'
......
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") - commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title } - commit_message = commit_message % { page_title: @page.title }
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f|
= form_errors(@page) = form_errors(@page)
- if @page.persisted? - if @page.persisted?
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
.note-text.md .note-text.md
= markdown_field(note, :note) = markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago') = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore, markdown_version: note.cached_markdown_version } }
#{note.note} #{note.note}
- if note_editable - if note_editable
= render 'shared/notes/edit', note: note = render 'shared/notes/edit', note: note
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
.snippet-form-holder .snippet-form-holder
= form_for @snippet, url: url, html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f| = form_for @snippet, url: url,
html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
data: { markdown_version: @snippet.cached_markdown_version } do |f|
= form_errors(@snippet) = form_errors(@snippet)
.form-group.row .form-group.row
......
...@@ -4,9 +4,11 @@ module RepositoryCheck ...@@ -4,9 +4,11 @@ module RepositoryCheck
class BatchWorker class BatchWorker
include ApplicationWorker include ApplicationWorker
include RepositoryCheckQueue include RepositoryCheckQueue
include ExclusiveLeaseGuard
RUN_TIME = 3600 RUN_TIME = 3600
BATCH_SIZE = 10_000 BATCH_SIZE = 10_000
LEASE_TIMEOUT = 1.hour
attr_reader :shard_name attr_reader :shard_name
...@@ -16,6 +18,20 @@ module RepositoryCheck ...@@ -16,6 +18,20 @@ module RepositoryCheck
return unless Gitlab::CurrentSettings.repository_checks_enabled return unless Gitlab::CurrentSettings.repository_checks_enabled
return unless Gitlab::ShardHealthCache.healthy_shard?(shard_name) return unless Gitlab::ShardHealthCache.healthy_shard?(shard_name)
try_obtain_lease do
perform_repository_checks
end
end
def lease_timeout
LEASE_TIMEOUT
end
def lease_key
"repository_check_batch_worker:#{shard_name}"
end
def perform_repository_checks
start = Time.now start = Time.now
# This loop will break after a little more than one hour ('a little # This loop will break after a little more than one hour ('a little
...@@ -26,7 +42,7 @@ module RepositoryCheck ...@@ -26,7 +42,7 @@ module RepositoryCheck
project_ids.each do |project_id| project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME break if Time.now - start >= RUN_TIME
next unless try_obtain_lease(project_id) next unless try_obtain_lease_for_project(project_id)
SingleRepositoryWorker.new.perform(project_id) SingleRepositoryWorker.new.perform(project_id)
end end
...@@ -60,7 +76,7 @@ module RepositoryCheck ...@@ -60,7 +76,7 @@ module RepositoryCheck
Project.where(repository_storage: shard_name) Project.where(repository_storage: shard_name)
end end
def try_obtain_lease(id) def try_obtain_lease_for_project(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is # Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel. # super slow we definitely do not want to run it twice in parallel.
Gitlab::ExclusiveLease.new( Gitlab::ExclusiveLease.new(
......
...@@ -3,13 +3,22 @@ module RepositoryCheck ...@@ -3,13 +3,22 @@ module RepositoryCheck
include ApplicationWorker include ApplicationWorker
include CronjobQueue include CronjobQueue
include ::EachShardWorker include ::EachShardWorker
include ExclusiveLeaseGuard
LEASE_TIMEOUT = 1.hour
def perform def perform
return unless Gitlab::CurrentSettings.repository_checks_enabled return unless Gitlab::CurrentSettings.repository_checks_enabled
each_eligible_shard do |shard_name| try_obtain_lease do
RepositoryCheck::BatchWorker.perform_async(shard_name) each_eligible_shard do |shard_name|
RepositoryCheck::BatchWorker.perform_async(shard_name)
end
end end
end end
def lease_timeout
LEASE_TIMEOUT
end
end end
end end
---
title: Add Object Storage to project export
merge_request: 20105
author:
type: added
---
title: Resolve compatibility issues with node 6
merge_request: 20461
author:
type: fixed
---
title: Stop relying on migrations in the CacheableAttributes cache key and cache attributes
for 1 minute instead
merge_request: 20389
author:
type: fixed
---
title: Improve render performance of large wiki pages
merge_request: 20465
author: Peter Leitzen
type: performance
...@@ -39,7 +39,7 @@ Rails.application.configure do ...@@ -39,7 +39,7 @@ Rails.application.configure do
config.action_mailer.delivery_method = :letter_opener_web config.action_mailer.delivery_method = :letter_opener_web
# Don't make a mess when bootstrapping a development environment # Don't make a mess when bootstrapping a development environment
config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1')
config.action_mailer.preview_path = 'spec/mailers/previews' config.action_mailer.preview_path = 'app/mailers/previews'
config.eager_load = false config.eager_load = false
......
...@@ -160,6 +160,9 @@ production: &base ...@@ -160,6 +160,9 @@ production: &base
# aws_access_key_id: AWS_ACCESS_KEY_ID # aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY # aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: us-east-1 # region: us-east-1
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# endpoint: 'https://s3.amazonaws.com' # default: nil - Useful for S3 compliant services such as DigitalOcean Spaces
## Git LFS ## Git LFS
lfs: lfs:
...@@ -180,6 +183,7 @@ production: &base ...@@ -180,6 +183,7 @@ production: &base
# Use the following options to configure an AWS compatible host # Use the following options to configure an AWS compatible host
# host: 'localhost' # default: s3.amazonaws.com # host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil # endpoint: 'http://127.0.0.1:9000' # default: nil
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Uploads (attachments, avatars, etc...) ## Uploads (attachments, avatars, etc...)
...@@ -197,6 +201,7 @@ production: &base ...@@ -197,6 +201,7 @@ production: &base
provider: AWS provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY aws_secret_access_key: AWS_SECRET_ACCESS_KEY
aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
region: us-east-1 region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com # host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil # endpoint: 'http://127.0.0.1:9000' # default: nil
......
class CreateImportExportUploads < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :import_export_uploads do |t|
t.datetime_with_timezone :updated_at, null: false
t.references :project, index: true, foreign_key: { on_delete: :cascade }, unique: true
t.text :import_file
t.text :export_file
end
add_index :import_export_uploads, :updated_at
end
end
...@@ -949,6 +949,16 @@ ActiveRecord::Schema.define(version: 20180702120647) do ...@@ -949,6 +949,16 @@ ActiveRecord::Schema.define(version: 20180702120647) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "import_export_uploads", force: :cascade do |t|
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.text "import_file"
t.text "export_file"
end
add_index "import_export_uploads", ["project_id"], name: "index_import_export_uploads_on_project_id", using: :btree
add_index "import_export_uploads", ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
create_table "internal_ids", id: :bigserial, force: :cascade do |t| create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
t.integer "usage", null: false t.integer "usage", null: false
...@@ -2252,6 +2262,7 @@ ActiveRecord::Schema.define(version: 20180702120647) do ...@@ -2252,6 +2262,7 @@ ActiveRecord::Schema.define(version: 20180702120647) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "import_export_uploads", "projects", on_delete: :cascade
add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
......
...@@ -30,5 +30,12 @@ sudo gitlab-rake gitlab:import_export:data ...@@ -30,5 +30,12 @@ sudo gitlab-rake gitlab:import_export:data
bundle exec rake gitlab:import_export:data RAILS_ENV=production bundle exec rake gitlab:import_export:data RAILS_ENV=production
``` ```
In order to enable Object Storage on the Export, you can use the [feature flag][feature-flags]:
```
import_export_object_storage
```
[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050 [ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
[feature-flags]: https://docs.gitlab.com/ee/api/features.html
[tmp]: ../../development/shared_files.md [tmp]: ../../development/shared_files.md
...@@ -10,12 +10,12 @@ To view rendered emails "sent" in your development instance, visit ...@@ -10,12 +10,12 @@ To view rendered emails "sent" in your development instance, visit
Rails provides a way to preview our mailer templates in HTML and plaintext using Rails provides a way to preview our mailer templates in HTML and plaintext using
dummy data. dummy data.
The previews live in [`spec/mailers/previews`][previews] and can be viewed at The previews live in [`app/mailers/previews`][previews] and can be viewed at
[`/rails/mailers`](http://localhost:3000/rails/mailers). [`/rails/mailers`](http://localhost:3000/rails/mailers).
See the [Rails guides] for more info. See the [Rails guides] for more info.
[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews [previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/mailers/previews
[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails [Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails
## Incoming email ## Incoming email
......
...@@ -60,7 +60,7 @@ Libraries with the following licenses are acceptable for use: ...@@ -60,7 +60,7 @@ Libraries with the following licenses are acceptable for use:
## Unacceptable Licenses ## Unacceptable Licenses
Libraries with the following licenses are unacceptable for use: Libraries with the following licenses require legal approval for use:
- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
...@@ -68,6 +68,26 @@ Libraries with the following licenses are unacceptable for use: ...@@ -68,6 +68,26 @@ Libraries with the following licenses are unacceptable for use:
- [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation. - [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation.
- [WTFPL][WTFPL]: is a public domain dedication [rejected by the OSI (3.2)][WTFPL-OSI]. Also has a strong language which is not in accordance with our diversity policy. - [WTFPL][WTFPL]: is a public domain dedication [rejected by the OSI (3.2)][WTFPL-OSI]. Also has a strong language which is not in accordance with our diversity policy.
## GPL Cooperation Commitment
Before filing or continuing to prosecute any legal proceeding or claim (other than a Defensive Action) arising from termination of a Covered License, GitLab commits to extend to the person or entity (“you”) accused of violating the Covered License the following provisions regarding cure and reinstatement, taken from GPL version 3. As used here, the term ‘this License’ refers to the specific Covered License being enforced.
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
GitLab intends this Commitment to be irrevocable, and binding and enforceable against GitLab and assignees of or successors to GitLab’s copyrights.
GitLab may modify this Commitment by publishing a new edition on this page or a successor location.
Definitions
‘Covered License’ means the GNU General Public License, version 2 (GPLv2), the GNU Lesser General Public License, version 2.1 (LGPLv2.1), or the GNU Library General Public License, version 2 (LGPLv2), all as published by the Free Software Foundation.
‘Defensive Action’ means a legal proceeding or claim that GitLab brings against you in response to a prior proceeding or claim initiated by you or your affiliate.
GitLab means GitLab Inc. and its affiliates and subsidiaries.
## Requesting Approval for Licenses ## Requesting Approval for Licenses
Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please email `legal@gitlab.com` with the details. After a decision has been made, the original requestor is responsible for updating this document. Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please email `legal@gitlab.com` with the details. After a decision has been made, the original requestor is responsible for updating this document.
......
# Integrate your GitLab server with Bitbucket # Integrate your GitLab server with Bitbucket
NOTE: **Note:**
You need to [enable OmniAuth](omniauth.md) in order to use this.
Import projects from Bitbucket.org and login to your GitLab instance with your Import projects from Bitbucket.org and login to your GitLab instance with your
Bitbucket.org account. Bitbucket.org account.
...@@ -76,13 +79,13 @@ you to use. ...@@ -76,13 +79,13 @@ you to use.
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
``` ```
1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
for initial settings.
1. Add the Bitbucket provider configuration: 1. Add the Bitbucket provider configuration:
For Omnibus packages: For Omnibus packages:
```ruby ```ruby
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
"name" => "bitbucket", "name" => "bitbucket",
...@@ -96,10 +99,13 @@ you to use. ...@@ -96,10 +99,13 @@ you to use.
For installations from source: For installations from source:
```yaml ```yaml
- { name: 'bitbucket', omniauth:
app_id: 'BITBUCKET_APP_KEY', enabled: true
app_secret: 'BITBUCKET_APP_SECRET', providers:
url: 'https://bitbucket.org/' } - { name: 'bitbucket',
app_id: 'BITBUCKET_APP_KEY',
app_secret: 'BITBUCKET_APP_SECRET',
url: 'https://bitbucket.org/' }
``` ```
--- ---
...@@ -121,6 +127,9 @@ well, the user will be returned to GitLab and will be signed in. ...@@ -121,6 +127,9 @@ well, the user will be returned to GitLab and will be signed in.
Once the above configuration is set up, you can use Bitbucket to sign into Once the above configuration is set up, you can use Bitbucket to sign into
GitLab and [start importing your projects][bb-import]. GitLab and [start importing your projects][bb-import].
If you don't want to enable signing in with Bitbucket but just want to import
projects from Bitbucket, you could [disable it in the admin panel](omniauth.md#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
[init-oauth]: omniauth.md#initial-omniauth-configuration [init-oauth]: omniauth.md#initial-omniauth-configuration
[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md [bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md [bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
......
This diff is collapsed.
...@@ -206,7 +206,7 @@ kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalanc ...@@ -206,7 +206,7 @@ kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalanc
> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run: > **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
> ```bash > ```bash
> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. > kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}".
> ``` > ```
The output is the external IP address of your cluster. This information can then The output is the external IP address of your cluster. This information can then
......
...@@ -23,9 +23,13 @@ module API ...@@ -23,9 +23,13 @@ module API
get ':id/export/download' do get ':id/export/download' do
path = user_project.export_project_path path = user_project.export_project_path
render_api_error!('404 Not found or has expired', 404) unless path if path
present_disk_file!(path, File.basename(path), 'application/gzip')
present_disk_file!(path, File.basename(path), 'application/gzip') elsif user_project.export_project_object_exists?
present_carrierwave_file!(user_project.import_export_upload.export_file)
else
render_api_error!('404 Not found or has expired', 404)
end
end end
desc 'Start export' do desc 'Start export' do
......
...@@ -100,6 +100,11 @@ module Banzai ...@@ -100,6 +100,11 @@ module Banzai
ref_pattern = object_class.reference_pattern ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern link_pattern = object_class.link_reference_pattern
# Compile often used regexps only once outside of the loop
ref_pattern_anchor = /\A#{ref_pattern}\z/
link_pattern_start = /\A#{link_pattern}/
link_pattern_anchor = /\A#{link_pattern}\z/
nodes.each do |node| nodes.each do |node|
if text_node?(node) && ref_pattern if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content| replace_text_when_pattern_matches(node, ref_pattern) do |content|
...@@ -108,7 +113,7 @@ module Banzai ...@@ -108,7 +113,7 @@ module Banzai
elsif element_node?(node) elsif element_node?(node)
yield_valid_link(node) do |link, inner_html| yield_valid_link(node) do |link, inner_html|
if ref_pattern && link =~ /\A#{ref_pattern}\z/ if ref_pattern && link =~ ref_pattern_anchor
replace_link_node_with_href(node, link) do replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_content: inner_html) object_link_filter(link, ref_pattern, link_content: inner_html)
end end
...@@ -118,7 +123,7 @@ module Banzai ...@@ -118,7 +123,7 @@ module Banzai
next unless link_pattern next unless link_pattern
if link == inner_html && inner_html =~ /\A#{link_pattern}/ if link == inner_html && inner_html =~ link_pattern_start
replace_link_node_with_text(node, link) do replace_link_node_with_text(node, link) do
object_link_filter(inner_html, link_pattern, link_reference: true) object_link_filter(inner_html, link_pattern, link_reference: true)
end end
...@@ -126,7 +131,7 @@ module Banzai ...@@ -126,7 +131,7 @@ module Banzai
next next
end end
if link =~ /\A#{link_pattern}\z/ if link =~ link_pattern_anchor
replace_link_node_with_href(node, link) do replace_link_node_with_href(node, link) do
object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true) object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
end end
......
...@@ -9,10 +9,6 @@ module Gitlab ...@@ -9,10 +9,6 @@ module Gitlab
Settings Settings
end end
def self.migrations_hash
@_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
end
def self.revision def self.revision
@_revision ||= begin @_revision ||= begin
if File.exist?(root.join("REVISION")) if File.exist?(root.join("REVISION"))
......
...@@ -42,6 +42,21 @@ module Gitlab ...@@ -42,6 +42,21 @@ module Gitlab
!self.read_only? !self.read_only?
end end
# check whether the underlying database is in read-only mode
def self.db_read_only?
if postgresql?
ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()')
.first
.fetch('pg_is_in_recovery') == 't'
else
false
end
end
def self.db_read_write?
!self.db_read_only?
end
def self.version def self.version
@version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end end
......
...@@ -40,6 +40,10 @@ module Gitlab ...@@ -40,6 +40,10 @@ module Gitlab
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz" "#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end end
def object_storage?
Feature.enabled?(:import_export_object_storage)
end
def version def version
VERSION VERSION
end end
......
...@@ -2,6 +2,7 @@ module Gitlab ...@@ -2,6 +2,7 @@ module Gitlab
module ImportExport module ImportExport
module AfterExportStrategies module AfterExportStrategies
class BaseAfterExportStrategy class BaseAfterExportStrategy
extend Gitlab::ImportExport::CommandLineUtil
include ActiveModel::Validations include ActiveModel::Validations
extend Forwardable extend Forwardable
...@@ -24,9 +25,10 @@ module Gitlab ...@@ -24,9 +25,10 @@ module Gitlab
end end
def execute(current_user, project) def execute(current_user, project)
return unless project&.export_project_path
@project = project @project = project
return unless @project.export_status == :finished
@current_user = current_user @current_user = current_user
if invalid? if invalid?
...@@ -51,9 +53,12 @@ module Gitlab ...@@ -51,9 +53,12 @@ module Gitlab
end end
def self.lock_file_path(project) def self.lock_file_path(project)
return unless project&.export_path return unless project.export_path || object_storage?
File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME) lock_path = project.import_export_shared.archive_path
mkdir_p(lock_path)
File.join(lock_path, AFTER_EXPORT_LOCK_FILE_NAME)
end end
protected protected
...@@ -77,6 +82,10 @@ module Gitlab ...@@ -77,6 +82,10 @@ module Gitlab
def log_validation_errors def log_validation_errors
errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) } errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
end end
def object_storage?
project.export_project_object_exists?
end
end end
end end
end end
......
...@@ -38,14 +38,20 @@ module Gitlab ...@@ -38,14 +38,20 @@ module Gitlab
private private
def send_file def send_file
export_file = File.open(project.export_project_path) Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options) # rubocop:disable GitlabSecurity/PublicSend
Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options(export_file)) # rubocop:disable GitlabSecurity/PublicSend
ensure ensure
export_file.close if export_file export_file.close if export_file && !object_storage?
end
def export_file
if object_storage?
project.import_export_upload.export_file.file.open
else
File.open(project.export_project_path)
end
end end
def send_file_options(export_file) def send_file_options
{ {
body_stream: export_file, body_stream: export_file,
headers: headers headers: headers
...@@ -53,7 +59,15 @@ module Gitlab ...@@ -53,7 +59,15 @@ module Gitlab
end end
def headers def headers
{ 'Content-Length' => File.size(project.export_project_path).to_s } { 'Content-Length' => export_size.to_s }
end
def export_size
if object_storage?
project.import_export_upload.export_file.file.size
else
File.size(project.export_project_path)
end
end end
end end
end end
......
...@@ -15,15 +15,22 @@ module Gitlab ...@@ -15,15 +15,22 @@ module Gitlab
def save def save
if compress_and_save if compress_and_save
remove_export_path remove_export_path
Rails.logger.info("Saved project export #{archive_file}") Rails.logger.info("Saved project export #{archive_file}")
archive_file
save_on_object_storage if use_object_storage?
else else
@shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}")) @shared.error(Gitlab::ImportExport::Error.new(error_message))
false false
end end
rescue => e rescue => e
@shared.error(e) @shared.error(e)
false false
ensure
if use_object_storage?
remove_archive
remove_export_path
end
end end
private private
...@@ -36,9 +43,29 @@ module Gitlab ...@@ -36,9 +43,29 @@ module Gitlab
FileUtils.rm_rf(@shared.export_path) FileUtils.rm_rf(@shared.export_path)
end end
def remove_archive
FileUtils.rm_rf(@shared.archive_path)
end
def archive_file def archive_file
@archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project)) @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
end end
def save_on_object_storage
upload = ImportExportUpload.find_or_initialize_by(project: @project)
File.open(archive_file) { |file| upload.export_file = file }
upload.save!
end
def use_object_storage?
Gitlab::ImportExport.object_storage?
end
def error_message
"Unable to save #{archive_file} into #{@shared.export_path}. Object storage enabled: #{use_object_storage?}"
end
end end
end end
end end
...@@ -69,6 +69,7 @@ module Gitlab ...@@ -69,6 +69,7 @@ module Gitlab
@route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {} @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
end end
# Overridden in EE module
def whitelisted_routes def whitelisted_routes
grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end end
......
...@@ -790,23 +790,55 @@ describe ProjectsController do ...@@ -790,23 +790,55 @@ describe ProjectsController do
project.add_master(user) project.add_master(user)
end end
context 'when project export is enabled' do context 'object storage disabled' do
it 'returns 302' do before do
get :download_export, namespace_id: project.namespace, id: project stub_feature_flags(import_export_object_storage: false)
end
expect(response).to have_gitlab_http_status(302) context 'when project export is enabled' do
it 'returns 302' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(302)
end
end
context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
end
it 'returns 404' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(404)
end
end end
end end
context 'when project export is disabled' do context 'object storage enabled' do
before do before do
stub_application_setting(project_export_enabled?: false) stub_feature_flags(import_export_object_storage: true)
end end
it 'returns 404' do context 'when project export is enabled' do
get :download_export, namespace_id: project.namespace, id: project it 'returns 302' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(302)
end
end
context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
end
it 'returns 404' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(404)
end
end end
end end
end end
......
FactoryBot.define do
factory :import_export_upload do
project { create(:project) }
end
end
...@@ -103,6 +103,22 @@ FactoryBot.define do ...@@ -103,6 +103,22 @@ FactoryBot.define do
end end
trait :with_export do trait :with_export do
before(:create) do |_project, _evaluator|
allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { false }
allow(Feature).to receive(:enabled?).with('import_export_object_storage') { false }
end
after(:create) do |project, _evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
end
trait :with_object_export do
before(:create) do |_project, _evaluator|
allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { true }
allow(Feature).to receive(:enabled?).with('import_export_object_storage') { true }
end
after(:create) do |project, evaluator| after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id) ProjectExportWorker.new.perform(project.creator.id, project.id)
end end
......
...@@ -25,6 +25,7 @@ describe 'Import/Export - project export integration test', :js do ...@@ -25,6 +25,7 @@ describe 'Import/Export - project export integration test', :js do
before do before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
stub_feature_flags(import_export_object_storage: false)
end end
after do after do
......
...@@ -5,6 +5,7 @@ describe 'Import/Export - Namespace export file cleanup', :js do ...@@ -5,6 +5,7 @@ describe 'Import/Export - Namespace export file cleanup', :js do
before do before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
stub_feature_flags(import_export_object_storage: false)
end end
after do after do
......
...@@ -9,6 +9,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -9,6 +9,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
[relative link 1](../relative) [relative link 1](../relative)
[relative link 2](./relative) [relative link 2](./relative)
[relative link 3](./e/f/relative) [relative link 3](./e/f/relative)
[spaced link](title with spaces)
HEREDOC HEREDOC
end end
...@@ -42,6 +43,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -42,6 +43,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end end
end end
...@@ -64,6 +66,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -64,6 +66,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end end
end end
...@@ -86,6 +89,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -86,6 +89,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end end
end end
end end
...@@ -119,6 +123,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -119,6 +123,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end end
end end
...@@ -136,6 +141,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -136,6 +141,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end end
end end
...@@ -153,6 +159,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do ...@@ -153,6 +159,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end end
end end
end end
......
...@@ -68,6 +68,26 @@ describe 'Snippet', :js do ...@@ -68,6 +68,26 @@ describe 'Snippet', :js do
end end
end end
context 'with cached Redcarpet html' do
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION) }
let(:file_name) { 'test.md' }
let(:content) { "1. one\n - sublist\n" }
it 'renders correctly' do
expect(page).to have_xpath("//ol//li//ul")
end
end
context 'with cached CommonMark html' do
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
let(:file_name) { 'test.md' }
let(:content) { "1. one\n - sublist\n" }
it 'renders correctly' do
expect(page).not_to have_xpath("//ol//li//ul")
end
end
context 'switching to the simple viewer' do context 'switching to the simple viewer' do
before do before do
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
......
...@@ -184,6 +184,7 @@ describe IssuablesHelper do ...@@ -184,6 +184,7 @@ describe IssuablesHelper do
issuableRef: "##{issue.iid}", issuableRef: "##{issue.iid}",
markdownPreviewPath: "/#{@project.full_path}/preview_markdown", markdownPreviewPath: "/#{@project.full_path}/preview_markdown",
markdownDocsPath: '/help/user/markdown', markdownDocsPath: '/help/user/markdown',
markdownVersion: 11,
issuableTemplates: [], issuableTemplates: [],
projectPath: @project.path, projectPath: @project.path,
projectNamespace: @project.namespace.path, projectNamespace: @project.namespace.path,
......
...@@ -205,7 +205,9 @@ describe MarkupHelper do ...@@ -205,7 +205,9 @@ describe MarkupHelper do
it "uses Wiki pipeline for markdown files" do it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown) allow(@wiki).to receive(:format).and_return(:markdown)
expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", issuable_state_filter_enabled: true) expect(helper).to receive(:markdown_unsafe).with('wiki content',
pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page",
issuable_state_filter_enabled: true, markdown_engine: :redcarpet)
helper.render_wiki_content(@wiki) helper.render_wiki_content(@wiki)
end end
...@@ -236,19 +238,32 @@ describe MarkupHelper do ...@@ -236,19 +238,32 @@ describe MarkupHelper do
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8') expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
end end
it "delegates to #markdown_unsafe when file name corresponds to Markdown" do it 'delegates to #markdown_unsafe when file name corresponds to Markdown' do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true) expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown_unsafe).and_return('NOEL') expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
expect(helper.markup('foo.md', content)).to eq('NOEL') expect(helper.markup('foo.md', content)).to eq('NOEL')
end end
it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do it 'delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc' do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true) expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL') expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
expect(helper.markup('foo.adoc', content)).to eq('NOEL') expect(helper.markup('foo.adoc', content)).to eq('NOEL')
end end
it 'uses passed in rendered content' do
expect(helper).not_to receive(:gitlab_markdown?)
expect(helper).not_to receive(:markdown_unsafe)
expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>')
end
it 'defaults to Redcarpet' do
expect(helper).to receive(:markdown_unsafe).with(content, hash_including(markdown_engine: :redcarpet)).and_return('NOEL')
expect(helper.markup('foo.md', content)).to eq('NOEL')
end
end end
describe '#first_line_in_markdown' do describe '#first_line_in_markdown' do
......
...@@ -165,6 +165,7 @@ export const note = { ...@@ -165,6 +165,7 @@ export const note = {
report_abuse_path: report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/546', path: '/gitlab-org/gitlab-ce/notes/546',
cached_markdown_version: 11,
}; };
export const discussionMock = { export const discussionMock = {
......
...@@ -153,7 +153,7 @@ describe('Deployment component', () => { ...@@ -153,7 +153,7 @@ describe('Deployment component', () => {
it('renders external URL', () => { it('renders external URL', () => {
expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deploymentMockData.external_url); expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deploymentMockData.external_url);
expect(el.querySelector('.js-deploy-url').innerText).toContain(deploymentMockData.external_url_formatted); expect(el.querySelector('.js-deploy-url').innerText).toContain('View app');
}); });
it('renders stop button', () => { it('renders stop button', () => {
......
...@@ -145,7 +145,7 @@ describe('MRWidgetHeader', () => { ...@@ -145,7 +145,7 @@ describe('MRWidgetHeader', () => {
it('renders web ide button', () => { it('renders web ide button', () => {
const button = vm.$el.querySelector('.js-web-ide'); const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Web IDE'); expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
}); });
...@@ -154,7 +154,7 @@ describe('MRWidgetHeader', () => { ...@@ -154,7 +154,7 @@ describe('MRWidgetHeader', () => {
const button = vm.$el.querySelector('.js-web-ide'); const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Web IDE'); expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
}); });
...@@ -253,8 +253,8 @@ describe('MRWidgetHeader', () => { ...@@ -253,8 +253,8 @@ describe('MRWidgetHeader', () => {
}); });
it('renders diverged commits info', () => { it('renders diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual( expect(vm.$el.querySelector('.diverged-commits-count').textContent).toMatch(
'(12 commits behind)', /(mr-widget-refactor[\s\S]+?is 12 commits behind[\s\S]+?master)/,
); );
}); });
}); });
......
...@@ -3,17 +3,61 @@ require 'spec_helper' ...@@ -3,17 +3,61 @@ require 'spec_helper'
describe Banzai::Filter::MarkdownFilter do describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper include FilterSpecHelper
context 'code block' do describe 'markdown engine from context' do
it 'adds language to lang attribute when specified' do it 'defaults to CommonMark' do
result = filter("```html\nsome code\n```") expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test')
expect(result).to start_with("<pre><code lang=\"html\">") filter('test')
end end
it 'does not add language to lang attribute when not specified' do it 'uses Redcarpet' do
result = filter("```\nsome code\n```") expect_any_instance_of(Banzai::Filter::MarkdownEngines::Redcarpet).to receive(:render).and_return('test')
expect(result).to start_with("<pre><code>") filter('test', { markdown_engine: :redcarpet })
end
it 'uses CommonMark' do
expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test')
filter('test', { markdown_engine: :common_mark })
end
end
describe 'code block' do
context 'using CommonMark' do
before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
end
it 'adds language to lang attribute when specified' do
result = filter("```html\nsome code\n```")
expect(result).to start_with("<pre><code lang=\"html\">")
end
it 'does not add language to lang attribute when not specified' do
result = filter("```\nsome code\n```")
expect(result).to start_with("<pre><code>")
end
end
context 'using Redcarpet' do
before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :redcarpet)
end
it 'adds language to lang attribute when specified' do
result = filter("```html\nsome code\n```")
expect(result).to start_with("\n<pre><code lang=\"html\">")
end
it 'does not add language to lang attribute when not specified' do
result = filter("```\nsome code\n```")
expect(result).to start_with("\n<pre><code>")
end
end end
end end
end end
...@@ -357,6 +357,35 @@ describe Gitlab::Database do ...@@ -357,6 +357,35 @@ describe Gitlab::Database do
end end
end end
describe '.db_read_only?' do
context 'when using PostgreSQL' do
before do
allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
expect(described_class).to receive(:postgresql?).and_return(true)
end
it 'detects a read only database' do
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }])
expect(described_class.db_read_only?).to be_truthy
end
it 'detects a read write database' do
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
expect(described_class.db_read_only?).to be_falsey
end
end
context 'when using MySQL' do
before do
expect(described_class).to receive(:postgresql?).and_return(false)
end
it { expect(described_class.db_read_only?).to be_falsey }
end
end
describe '#sanitize_timestamp' do describe '#sanitize_timestamp' do
let(:max_timestamp) { Time.at((1 << 31) - 1) } let(:max_timestamp) { Time.at((1 << 31) - 1) }
......
require 'spec_helper'
describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
let!(:service) { described_class.new }
let!(:project) { create(:project, :with_object_export) }
let(:shared) { project.import_export_shared }
let!(:user) { create(:user) }
describe '#execute' do
before do
allow(service).to receive(:strategy_execute)
stub_feature_flags(import_export_object_storage: true)
end
it 'returns if project exported file is not found' do
allow(project).to receive(:export_project_object_exists?).and_return(false)
expect(service).not_to receive(:strategy_execute)
service.execute(user, project)
end
it 'creates a lock file in the export dir' do
allow(service).to receive(:delete_after_export_lock)
service.execute(user, project)
expect(lock_path_exist?).to be_truthy
end
context 'when the method succeeds' do
it 'removes the lock file' do
service.execute(user, project)
expect(lock_path_exist?).to be_falsey
end
end
context 'when the method fails' do
before do
allow(service).to receive(:strategy_execute).and_call_original
end
context 'when validation fails' do
before do
allow(service).to receive(:invalid?).and_return(true)
end
it 'does not create the lock file' do
expect(service).not_to receive(:create_or_update_after_export_lock)
service.execute(user, project)
end
it 'does not execute main logic' do
expect(service).not_to receive(:strategy_execute)
service.execute(user, project)
end
it 'logs validation errors in shared context' do
expect(service).to receive(:log_validation_errors)
service.execute(user, project)
end
end
context 'when an exception is raised' do
it 'removes the lock' do
expect { service.execute(user, project) }.to raise_error(NotImplementedError)
expect(lock_path_exist?).to be_falsey
end
end
end
end
describe '#log_validation_errors' do
it 'add the message to the shared context' do
errors = %w(test_message test_message2)
allow(service).to receive(:invalid?).and_return(true)
allow(service.errors).to receive(:full_messages).and_return(errors)
expect(shared).to receive(:add_error_message).twice.and_call_original
service.execute(user, project)
expect(shared.errors).to eq errors
end
end
describe '#to_json' do
it 'adds the current strategy class to the serialized attributes' do
params = { param1: 1 }
result = params.merge(klass: described_class.to_s).to_json
expect(described_class.new(params).to_json).to eq result
end
end
def lock_path_exist?
File.exist?(described_class.lock_file_path(project))
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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