Commit abca813a authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'ce-to-ee-2017-10-27' into 'master'

CE upstream: Friday

Closes gitlab-ce#39188, gitaly#683, #3664, and gitlab-ce#39351

See merge request gitlab-org/gitlab-ee!3214
parents 43371992 0ff6f13e
...@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels) - [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging) - [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals) - [Feature proposals](#feature-proposals)
......
...@@ -414,7 +414,7 @@ group :ed25519 do ...@@ -414,7 +414,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.45.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.48.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -297,7 +297,7 @@ GEM ...@@ -297,7 +297,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.45.0) gitaly-proto (0.48.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1065,7 +1065,7 @@ DEPENDENCIES ...@@ -1065,7 +1065,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.45.0) gitaly-proto (~> 0.48.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
...@@ -265,7 +265,8 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -265,7 +265,8 @@ import initGroupAnalytics from './init_group_analytics';
break; break;
case 'projects:compare:show': case 'projects:compare:show':
new Diff(); new Diff();
initChangesDropdown(); const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
break; break;
case 'projects:branches:new': case 'projects:branches:new':
case 'projects:branches:create': case 'projects:branches:create':
......
import stickyMonitor from './lib/utils/sticky'; import stickyMonitor from './lib/utils/sticky';
export default () => { export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed')); stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({ $('.js-diff-stats-dropdown').glDropdown({
filterable: true, filterable: true,
......
...@@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { ...@@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
} }
}; };
export default (el, insertPlaceholder = true) => { export default (el, stickyTop, insertPlaceholder = true) => {
if (!el) return; if (!el) return;
const computedStyle = window.getComputedStyle(el); if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
if (!/sticky/.test(computedStyle.position)) return;
const stickyTop = parseInt(computedStyle.top, 10);
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true, passive: true,
......
...@@ -67,6 +67,10 @@ import Diff from './diff'; ...@@ -67,6 +67,10 @@ import Diff from './diff';
class MergeRequestTabs { class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const paddingTop = 16;
this.diffsLoaded = false; this.diffsLoaded = false;
this.pipelinesLoaded = false; this.pipelinesLoaded = false;
this.commitsLoaded = false; this.commitsLoaded = false;
...@@ -76,6 +80,11 @@ import Diff from './diff'; ...@@ -76,6 +80,11 @@ import Diff from './diff';
this.setCurrentAction = this.setCurrentAction.bind(this); this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this); this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this); this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
if (stubLocation) { if (stubLocation) {
location = stubLocation; location = stubLocation;
...@@ -278,7 +287,7 @@ import Diff from './diff'; ...@@ -278,7 +287,7 @@ import Diff from './diff';
const $container = $('#diffs'); const $container = $('#diffs');
$container.html(data.html); $container.html(data.html);
initChangesDropdown(); initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue'; import issueNoteForm from './issue_note_form.vue';
import placeholderNote from './issue_placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
import * as constants from '../constants'; import * as constants from '../constants';
import issueNote from './issue_note.vue'; import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue'; import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import issueCommentForm from './issue_comment_form.vue'; import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
components: { components: {
issueNote, issueNote,
issueDiscussion, issueDiscussion,
issueSystemNote, systemNote,
issueCommentForm, issueCommentForm,
loadingIcon, loadingIcon,
placeholderNote, placeholderNote,
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
} }
return placeholderNote; return placeholderNote;
} else if (note.individual_note) { } else if (note.individual_note) {
return note.notes[0].system ? issueSystemNote : issueNote; return note.notes[0].system ? systemNote : issueNote;
} }
return issueDiscussion; return issueDiscussion;
......
<script>
import RepoStore from '../../stores/repo_store';
import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub';
import newModal from './modal.vue';
export default {
components: {
newModal,
},
data() {
return {
openModal: false,
modalType: '',
currentPath: RepoStore.path,
};
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
createNewEntryInStore(name, type) {
RepoHelper.createNewEntry(name, type);
this.toggleModalOpen();
},
},
created() {
eventHub.$on('createNewEntry', this.createNewEntryInStore);
},
beforeDestroy() {
eventHub.$off('createNewEntry', this.createNewEntryInStore);
},
};
</script>
<template>
<div>
<ul class="breadcrumb repo-breadcrumb">
<li class="dropdown">
<button
type="button"
class="btn btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<i
class="fa fa-plus"
aria-hidden="true"
>
</i>
</button>
<ul class="dropdown-menu">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal
v-if="openModal"
:type="modalType"
:current-path="currentPath"
@toggle="toggleModalOpen"
/>
</div>
</template>
<script>
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import eventHub from '../../event_hub';
export default {
props: {
currentPath: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.currentPath !== '' ? `${this.currentPath}/` : '',
};
},
components: {
popupDialog,
},
methods: {
createEntryInStore() {
eventHub.$emit('createNewEntry', this.entryName, this.type);
},
toggleModalOpen() {
this.$emit('toggle');
},
},
computed: {
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
},
};
</script>
<template>
<popup-dialog
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@toggle="toggleModalOpen"
@submit="createEntryInStore"
>
<form
class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
</form>
</popup-dialog>
</template>
...@@ -46,6 +46,10 @@ export default { ...@@ -46,6 +46,10 @@ export default {
dialogSubmitted(status) { dialogSubmitted(status) {
this.toggleDialogOpen(false); this.toggleDialogOpen(false);
this.dialog.status = status; this.dialog.status = status;
// remove tmp files
Helper.removeAllTmpFiles('openedFiles');
Helper.removeAllTmpFiles('files');
}, },
toggleBlobView: Store.toggleBlobView, toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) { createNewBranch(branch) {
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
action: 'update', action: f.tempFile ? 'create' : 'update',
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
...@@ -62,7 +62,6 @@ export default { ...@@ -62,7 +62,6 @@ export default {
if (newBranch) { if (newBranch) {
payload.start_branch = this.currentBranch; payload.start_branch = this.currentBranch;
} }
this.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(() => { .then(() => {
this.resetCommitState(); this.resetCommitState();
...@@ -78,6 +77,8 @@ export default { ...@@ -78,6 +77,8 @@ export default {
}, },
tryCommit(e, skipBranchCheck = false, newBranch = false) { tryCommit(e, skipBranchCheck = false, newBranch = false) {
this.submitCommitsLoading = true;
if (skipBranchCheck) { if (skipBranchCheck) {
this.makeCommit(newBranch); this.makeCommit(newBranch);
} else { } else {
...@@ -90,6 +91,7 @@ export default { ...@@ -90,6 +91,7 @@ export default {
this.makeCommit(newBranch); this.makeCommit(newBranch);
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false;
Flash('An error occurred while committing your changes'); Flash('An error occurred while committing your changes');
}); });
} }
......
...@@ -16,7 +16,7 @@ const RepoEditor = { ...@@ -16,7 +16,7 @@ const RepoEditor = {
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile.raw_path) Service.getRaw(this.activeFile)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data; Store.activeFile.plain = rawResponse.data;
......
...@@ -11,7 +11,12 @@ const RepoFileButtons = { ...@@ -11,7 +11,12 @@ const RepoFileButtons = {
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
showButtons() {
return this.activeFile.raw_path ||
this.activeFile.blame_path ||
this.activeFile.commits_path ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() { rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw'; return this.binary ? 'Download' : 'Raw';
}, },
...@@ -30,7 +35,10 @@ export default RepoFileButtons; ...@@ -30,7 +35,10 @@ export default RepoFileButtons;
</script> </script>
<template> <template>
<div id="repo-file-buttons"> <div
v-if="showButtons"
class="repo-file-buttons"
>
<a <a
:href="activeFile.raw_path" :href="activeFile.raw_path"
target="_blank" target="_blank"
......
...@@ -18,8 +18,8 @@ const RepoTab = { ...@@ -18,8 +18,8 @@ const RepoTab = {
}, },
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times close-icon': !this.tab.changed, 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
'fa-circle unsaved-icon': this.tab.changed, 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
}; };
return tabChangedObj; return tabChangedObj;
}, },
...@@ -30,7 +30,7 @@ const RepoTab = { ...@@ -30,7 +30,7 @@ const RepoTab = {
Store.setActiveFiles(file); Store.setActiveFiles(file);
}, },
closeTab(file) { closeTab(file) {
if (file.changed) return; if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
......
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Flash from '../../flash'; import Flash from '../../flash';
...@@ -8,6 +7,7 @@ const RepoHelper = { ...@@ -8,6 +7,7 @@ const RepoHelper = {
getDefaultActiveFile() { getDefaultActiveFile() {
return { return {
id: '',
active: true, active: true,
binary: false, binary: false,
extension: '', extension: '',
...@@ -62,6 +62,7 @@ const RepoHelper = { ...@@ -62,6 +62,7 @@ const RepoHelper = {
}); });
RepoHelper.updateHistoryEntry(tree.url, title); RepoHelper.updateHistoryEntry(tree.url, title);
Store.path = tree.path;
}, },
setDirectoryToClosed(entry) { setDirectoryToClosed(entry) {
...@@ -96,8 +97,8 @@ const RepoHelper = { ...@@ -96,8 +97,8 @@ const RepoHelper = {
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']); if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) { if (data.path && !Store.isInitialRoot) {
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']); Store.isRoot = data.path === '/';
Store.isInitialRoot = Store.isRoot; Store.isInitialRoot = Store.isRoot;
} }
...@@ -110,7 +111,7 @@ const RepoHelper = { ...@@ -110,7 +111,7 @@ const RepoHelper = {
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
} else if (!Store.isPreviewView() && !data.render_error) { } else if (!Store.isPreviewView() && !data.render_error) {
Service.getRaw(data.raw_path) Service.getRaw(data)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data; data.plain = rawResponse.data;
...@@ -138,6 +139,10 @@ const RepoHelper = { ...@@ -138,6 +139,10 @@ const RepoHelper = {
addToDirectory(file, data) { addToDirectory(file, data) {
const tree = file || Store; const tree = file || Store;
// TODO: Figure out why `popstate` is being trigger in the specs
if (!tree.files) return;
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0)); const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
tree.files = files; tree.files = files;
...@@ -157,7 +162,18 @@ const RepoHelper = { ...@@ -157,7 +162,18 @@ const RepoHelper = {
}, },
serializeRepoEntity(type, entity, level = 0) { serializeRepoEntity(type, entity, level = 0) {
const { id, url, name, icon, last_commit, tree_url } = entity; const {
id,
url,
name,
icon,
last_commit,
tree_url,
path,
tempFile,
active,
opened,
} = entity;
return { return {
id, id,
...@@ -165,11 +181,14 @@ const RepoHelper = { ...@@ -165,11 +181,14 @@ const RepoHelper = {
name, name,
url, url,
tree_url, tree_url,
path,
level, level,
tempFile,
icon: `fa-${icon}`, icon: `fa-${icon}`,
files: [], files: [],
loading: false, loading: false,
opened: false, opened,
active,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
lastCommit: last_commit ? { lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`, url: `${Store.projectUrl}/commit/${last_commit.id}`,
...@@ -213,7 +232,7 @@ const RepoHelper = { ...@@ -213,7 +232,7 @@ const RepoHelper = {
}, },
findOpenedFileFromActive() { findOpenedFileFromActive() {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
}, },
getFileFromPath(path) { getFileFromPath(path) {
...@@ -223,6 +242,76 @@ const RepoHelper = { ...@@ -223,6 +242,76 @@ const RepoHelper = {
loadingError() { loadingError() {
Flash('Unable to load this content at this time.'); Flash('Unable to load this content at this time.');
}, },
openEditMode() {
Store.editMode = true;
Store.currentBlobView = 'repo-editor';
},
updateStorePath(path) {
Store.path = path;
},
findOrCreateEntry(type, tree, name) {
let exists = true;
let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name);
if (!foundEntry) {
foundEntry = RepoHelper.serializeRepoEntity(type, {
id: name,
name,
path: tree.path ? `${tree.path}/${name}` : name,
icon: type === 'tree' ? 'folder' : 'file-text-o',
tempFile: true,
opened: true,
active: true,
}, tree.level !== undefined ? tree.level + 1 : 0);
exists = false;
tree.files.push(foundEntry);
}
return {
entry: foundEntry,
exists,
};
},
removeAllTmpFiles(storeFilesKey) {
Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
},
createNewEntry(name, type) {
const originalPath = Store.path;
let entryName = name;
if (entryName.indexOf(`${originalPath}/`) !== 0) {
this.updateStorePath('');
} else {
entryName = entryName.replace(`${originalPath}/`, '');
}
if (entryName === '') return;
const fileName = type === 'tree' ? '.gitkeep' : entryName;
let tree = Store;
if (type === 'tree') {
const dirNames = entryName.split('/');
dirNames.forEach((dirName) => {
if (dirName === '') return;
tree = this.findOrCreateEntry('tree', tree, dirName).entry;
});
}
if ((type === 'tree' && tree.tempFile) || type === 'blob') {
const file = this.findOrCreateEntry('blob', tree, fileName);
if (!file.exists) {
this.setFile(file.entry, file.entry);
this.openEditMode();
}
}
this.updateStorePath(originalPath);
},
}; };
export default RepoHelper; export default RepoHelper;
...@@ -6,6 +6,7 @@ import Store from './stores/repo_store'; ...@@ -6,6 +6,7 @@ import Store from './stores/repo_store';
import Repo from './components/repo.vue'; import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue'; import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue'; import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
...@@ -28,6 +29,7 @@ function setInitialStore(data) { ...@@ -28,6 +29,7 @@ function setInitialStore(data) {
Store.service = Service; Store.service = Service;
Store.service.url = data.url; Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl; Store.service.refsUrl = data.refsUrl;
Store.path = data.currentPath;
Store.projectId = data.projectId; Store.projectId = data.projectId;
Store.projectName = data.projectName; Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
...@@ -63,6 +65,18 @@ function initRepoEditButton(el) { ...@@ -63,6 +65,18 @@ function initRepoEditButton(el) {
}); });
} }
function initNewDropdown(el) {
return new Vue({
el,
components: {
newDropdown,
},
render(createElement) {
return createElement('new-dropdown');
},
});
}
function initNewBranchForm() { function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown'); const el = document.querySelector('.js-new-branch-dropdown');
...@@ -86,6 +100,7 @@ function initNewBranchForm() { ...@@ -86,6 +100,7 @@ function initNewBranchForm() {
function initRepoBundle() { function initRepoBundle() {
const repo = document.getElementById('repo'); const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode'); const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
setInitialStore(repo.dataset); setInitialStore(repo.dataset);
addEventsForNonVueEls(); addEventsForNonVueEls();
initDropdowns(); initDropdowns();
...@@ -95,6 +110,7 @@ function initRepoBundle() { ...@@ -95,6 +110,7 @@ function initRepoBundle() {
initRepo(repo); initRepo(repo);
initRepoEditButton(editButton); initRepoEditButton(editButton);
initNewBranchForm(); initNewBranchForm();
initNewDropdown(newDropdownHolder);
} }
$(initRepoBundle); $(initRepoBundle);
......
...@@ -8,7 +8,7 @@ const RepoMixin = { ...@@ -8,7 +8,7 @@ const RepoMixin = {
changedFiles() { changedFiles() {
const changedFileList = this.openedFiles const changedFileList = this.openedFiles
.filter(file => file.changed); .filter(file => file.changed || file.tempFile);
return changedFileList; return changedFileList;
}, },
}, },
......
...@@ -16,8 +16,14 @@ const RepoService = { ...@@ -16,8 +16,14 @@ const RepoService = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/, richExtensionRegExp: /md/,
getRaw(url) { getRaw(file) {
return axios.get(url, { if (file.tempFile) {
return Promise.resolve({
data: '',
});
}
return axios.get(file.raw_path, {
// Stop Axios from parsing a JSON file into a JS object // Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
......
...@@ -39,6 +39,7 @@ const RepoStore = { ...@@ -39,6 +39,7 @@ const RepoStore = {
newMrTemplateUrl: '', newMrTemplateUrl: '',
branchChanged: false, branchChanged: false,
commitMessage: '', commitMessage: '',
path: '',
loading: { loading: {
tree: false, tree: false,
blob: false, blob: false,
...@@ -77,21 +78,23 @@ const RepoStore = { ...@@ -77,21 +78,23 @@ const RepoStore = {
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
Service.getRaw(file.raw_path) Service.getRaw(file)
.then((rawResponse) => { .then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data; RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data; Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError); }).catch(Helper.loadingError);
} }
if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); if (!file.loading && !file.tempFile) {
Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
}
RepoStore.binary = file.binary; RepoStore.binary = file.binary;
RepoStore.setActiveLine(-1); RepoStore.setActiveLine(-1);
}, },
setFileActivity(file, openedFile, i) { setFileActivity(file, openedFile, i) {
const activeFile = openedFile; const activeFile = openedFile;
activeFile.active = file.url === activeFile.url; activeFile.active = file.id === activeFile.id;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i); if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
...@@ -99,7 +102,7 @@ const RepoStore = { ...@@ -99,7 +102,7 @@ const RepoStore = {
}, },
setActiveFile(activeFile, i) { setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile); RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
RepoStore.activeFileIndex = i; RepoStore.activeFileIndex = i;
}, },
...@@ -121,6 +124,11 @@ const RepoStore = { ...@@ -121,6 +124,11 @@ const RepoStore = {
return openedFile.path !== file.path; return openedFile.path !== file.path;
}); });
// remove the file from the sidebar if it is a tempFile
if (file.tempFile) {
RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path));
}
// now activate the right tab based on what you closed. // now activate the right tab based on what you closed.
if (RepoStore.openedFiles.length === 0) { if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {}; RepoStore.activeFile = {};
...@@ -170,7 +178,7 @@ const RepoStore = { ...@@ -170,7 +178,7 @@ const RepoStore = {
// getters // getters
isActiveFile(file) { isActiveFile(file) {
return file && file.url === RepoStore.activeFile.url; return file && file.id === RepoStore.activeFile.id;
}, },
isPreviewView() { isPreviewView() {
......
<script> <script>
/**
* Common component to render a placeholder note and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `getUserData` getter that contains
* {
* path: String,
* avatar_url: String,
* name: String,
* username: String,
* }
*
* @example
* <placeholder-note
* :note="{body: 'This is a note'}"
* />
*/
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default { export default {
name: 'issuePlaceholderNote', name: 'placeholderNote',
props: { props: {
note: { note: {
type: Object, type: Object,
......
<script> <script>
/**
* Common component to render a placeholder system note.
*
* @example
* <placeholder-system-note
* :note="{ body: 'Commands are being applied'}"
* />
*/
export default { export default {
name: 'placeholderSystemNote', name: 'placeholderSystemNote',
props: { props: {
......
<script> <script>
/**
* Common component to render a system note, icon and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `targetNoteHash` getter
*
* @example
* <system-note
* :note="{
* id: String,
* author: Object,
* createdAt: String,
* note_html: String,
* system_note_icon_name: String
* }"
* />
*/
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import issueNoteHeader from './issue_note_header.vue'; import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
export default { export default {
name: 'systemNote', name: 'systemNote',
...@@ -24,7 +42,7 @@ ...@@ -24,7 +42,7 @@
return this.targetNoteHash === this.noteAnchorId; return this.targetNoteHash === this.noteAnchorId;
}, },
iconHtml() { iconHtml() {
return gl.utils.spriteIcon(this.note.system_note_icon_name); return spriteIcon(this.note.system_note_icon_name);
}, },
}, },
}; };
...@@ -46,7 +64,8 @@ ...@@ -46,7 +64,8 @@
:author="note.author" :author="note.author"
:created-at="note.created_at" :created-at="note.created_at"
:note-id="note.id" :note-id="note.id"
:action-text-html="note.note_html" /> :action-text-html="note.note_html"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
}, },
text: { text: {
type: String, type: String,
required: true, required: false,
}, },
kind: { kind: {
type: String, type: String,
...@@ -82,14 +82,15 @@ export default { ...@@ -82,14 +82,15 @@ export default {
type="button" type="button"
class="btn" class="btn"
:class="btnCancelKindClass" :class="btnCancelKindClass"
@click="emitSubmit(false)"> @click="close">
{{closeButtonLabel}} {{ closeButtonLabel }}
</button> </button>
<button type="button" <button
type="button"
class="btn" class="btn"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit(true)">
{{primaryButtonLabel}} {{ primaryButtonLabel }}
</button> </button>
</div> </div>
</div> </div>
......
...@@ -12,12 +12,14 @@ ...@@ -12,12 +12,14 @@
:img-alt="tooltipText" :img-alt="tooltipText"
:img-size="20" :img-size="20"
:tooltip-text="tooltipText" :tooltip-text="tooltipText"
tooltip-placement="top" :tooltip-placement="top"
:username="username"
/> />
*/ */
import userAvatarImage from './user_avatar_image.vue'; import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
export default { export default {
name: 'UserAvatarLink', name: 'UserAvatarLink',
...@@ -60,6 +62,22 @@ export default { ...@@ -60,6 +62,22 @@ export default {
required: false, required: false,
default: 'top', default: 'top',
}, },
username: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldShowUsername() {
return this.username.length > 0;
},
avatarTooltipText() {
return this.shouldShowUsername ? '' : this.tooltipText;
},
},
directives: {
tooltip,
}, },
}; };
</script> </script>
...@@ -73,8 +91,13 @@ export default { ...@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt" :img-alt="imgAlt"
:css-classes="imgCssClasses" :css-classes="imgCssClasses"
:size="imgSize" :size="imgSize"
:tooltip-text="tooltipText" :tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement" :tooltip-placement="tooltipPlacement"
/> /><span
v-if="shouldShowUsername"
v-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
>{{username}}</span>
</a> </a>
</template> </template>
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
display: flex; display: flex;
flex: 1; flex: 1;
-webkit-flex: 1; -webkit-flex: 1;
padding-left: 30px; padding-left: 12px;
position: relative; position: relative;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -221,10 +221,6 @@ ...@@ -221,10 +221,6 @@
box-shadow: 0 0 4px $search-input-focus-shadow-color; box-shadow: 0 0 4px $search-input-focus-shadow-color;
} }
&.focus .fa-filter {
color: $common-gray-dark;
}
gl-emoji { gl-emoji {
display: inline-block; display: inline-block;
font-family: inherit; font-family: inherit;
...@@ -251,13 +247,6 @@ ...@@ -251,13 +247,6 @@
} }
} }
.fa-filter {
position: absolute;
top: 10px;
left: 10px;
color: $gray-darkest;
}
.fa-times { .fa-times {
right: 10px; right: 10px;
color: $gray-darkest; color: $gray-darkest;
......
...@@ -249,13 +249,12 @@ ...@@ -249,13 +249,12 @@
width: 100%; width: 100%;
padding-right: 5px; padding-right: 5px;
} }
} }
.discussion-actions { .discussion-actions {
display: table; display: table;
.new-issue-for-discussion path { .btn-default path {
fill: $gray-darkest; fill: $gray-darkest;
} }
......
...@@ -466,6 +466,10 @@ ul.notes { ...@@ -466,6 +466,10 @@ ul.notes {
float: right; float: right;
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
} }
.note-actions { .note-actions {
......
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
} }
} }
#repo-file-buttons { .repo-file-buttons {
background-color: $white-light; background-color: $white-light;
padding: 5px 10px; padding: 5px 10px;
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
......
...@@ -57,6 +57,10 @@ class HelpController < ApplicationController ...@@ -57,6 +57,10 @@ class HelpController < ApplicationController
def shortcuts def shortcuts
end end
def instance_configuration
@instance_configuration = InstanceConfiguration.new
end
def ui def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe') @user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end end
......
...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
tree_path = path_segments.join('/') tree_path = path_segments.join('/')
render json: json.merge( render json: json.merge(
id: @blob.id,
path: blob.path, path: blob.path,
name: blob.name, name: blob.name,
extension: blob.extension, extension: blob.extension,
......
...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController
format.json do format.json do
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
response.header['is-root'] = @path.empty?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
......
...@@ -23,7 +23,7 @@ class BranchesFinder ...@@ -23,7 +23,7 @@ class BranchesFinder
def filter_by_name(branches) def filter_by_name(branches)
if search if search
branches.select { |branch| branch.name.include?(search) } branches.select { |branch| branch.name.upcase.include?(search.upcase) }
else else
branches branches
end end
......
module InstanceConfigurationHelper
def instance_configuration_cell_html(value, &block)
return '-' unless value.to_s.presence
block_given? ? yield(value) : value
end
def instance_configuration_host(host)
@instance_configuration_host ||= instance_configuration_cell_html(host).capitalize
end
# Value must be in bytes
def instance_configuration_human_size_cell(value)
instance_configuration_cell_html(value) do |v|
number_to_human_size(v, strip_insignificant_zeros: true, significant: false)
end
end
end
...@@ -262,9 +262,7 @@ module Ci ...@@ -262,9 +262,7 @@ module Ci
end end
def commit def commit
@commit ||= project.commit(sha) @commit ||= project.commit_by(oid: sha)
rescue
nil
end end
def branch? def branch?
......
...@@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base ...@@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base
end end
def ref_path def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}"
end end
def formatted_external_url def formatted_external_url
......
require 'resolv'
class InstanceConfiguration
SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze
SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze
CACHE_KEY = 'instance_configuration'.freeze
EXPIRATION_TIME = 24.hours
def settings
@configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
gitlab_ci: gitlab_ci }.deep_symbolize_keys
end
end
private
def ssh_algorithms_hashes
SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact
end
def host
Settings.gitlab.host
end
def gitlab_pages
Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host))
end
def resolv_dns(dns)
Resolv.getaddress(dns)
rescue Resolv::ResolvError
end
def gitlab_ci
Settings.gitlab_ci
.to_h
.merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes,
default: 100.megabytes })
end
def ssh_algorithm_file(algorithm)
File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
end
def ssh_algorithm_hashes(algorithm)
content = ssh_algorithm_file_content(algorithm)
return unless content.present?
{ name: algorithm,
md5: ssh_algorithm_md5(content),
sha256: ssh_algorithm_sha256(content) }
end
def ssh_algorithm_file_content(algorithm)
file = ssh_algorithm_file(algorithm)
return unless File.exist?(file)
File.read(file)
end
def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
end
def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
end
end
...@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base ...@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base, key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc' algorithm: 'aes-256-cbc'
after_create :update after_create :update_daemon
after_save :update after_save :update_daemon
after_destroy :update after_destroy :update_daemon
def to_param def to_param
domain domain
...@@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base ...@@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base
private private
def update def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute ::Projects::UpdatePagesConfigurationService.new(project).execute
end end
......
...@@ -539,6 +539,10 @@ class Project < ActiveRecord::Base ...@@ -539,6 +539,10 @@ class Project < ActiveRecord::Base
repository.commit(ref) repository.commit(ref)
end end
def commit_by(oid:)
repository.commit_by(oid: oid)
end
# ref can't be HEAD, can only be branch/tag name or SHA # ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch) def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref) latest_pipeline = pipelines.latest_successful_for(ref)
...@@ -552,7 +556,7 @@ class Project < ActiveRecord::Base ...@@ -552,7 +556,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(first_commit_id, second_commit_id) def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id)
repository.commit(sha) if sha commit_by(oid: sha) if sha
end end
def saved? def saved?
......
...@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService ...@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated? validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true validates :api_url, url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
......
...@@ -155,7 +155,10 @@ class KubernetesService < DeploymentService ...@@ -155,7 +155,10 @@ class KubernetesService < DeploymentService
end end
def default_namespace def default_namespace
"#{project.path}-#{project.id}" if project.present? return unless project
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kubeclient!(api_path: 'api', api_version: 'v1')
......
...@@ -83,6 +83,7 @@ class Repository ...@@ -83,6 +83,7 @@ class Repository
@full_path = full_path @full_path = full_path
@disk_path = disk_path || full_path @disk_path = disk_path || full_path
@project = project @project = project
@commit_cache = {}
end end
def ==(other) def ==(other)
...@@ -110,18 +111,17 @@ class Repository ...@@ -110,18 +111,17 @@ class Repository
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
return ref if ref.is_a?(::Commit)
commit = find_commit(ref)
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end end
commit = ::Commit.new(commit, @project) if commit # Finding a commit by the passed SHA
commit # Also takes care of caching, based on the SHA
rescue Rugged::OdbError, Rugged::TreeError def commit_by(oid:)
nil return @commit_cache[oid] if @commit_cache.key?(oid)
@commit_cache[oid] = find_commit(oid)
end end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
...@@ -238,7 +238,7 @@ class Repository ...@@ -238,7 +238,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for # branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds. # example if they have comments or CI builds.
def keep_around(sha) def keep_around(sha)
return unless sha && commit(sha) return unless sha && commit_by(oid: sha)
return if kept_around?(sha) return if kept_around?(sha)
...@@ -869,22 +869,12 @@ class Repository ...@@ -869,22 +869,12 @@ class Repository
end end
def ff_merge(user, source, target_branch, merge_request: nil) def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target their_commit_id = commit(source)&.id
their_commit = raise 'Invalid merge source' if their_commit_id.nil?
if source.is_a?(Gitlab::Git::Commit)
source.raw_commit
else
rugged.lookup(source)
end
raise 'Invalid merge target' if our_commit.nil?
raise 'Invalid merge source' if their_commit.nil?
with_branch(user, target_branch) do |start_commit| merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
their_commit.oid with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
end end
def revert( def revert(
...@@ -1101,6 +1091,10 @@ class Repository ...@@ -1101,6 +1091,10 @@ class Repository
if instance_variable_defined?(ivar) if instance_variable_defined?(ivar)
instance_variable_get(ivar) instance_variable_get(ivar)
else else
# If the repository doesn't exist and a fallback was specified we return
# that value inmediately. This saves us Rugged/gRPC invocations.
return fallback unless fallback.nil? || exists?
begin begin
value = value =
if memoize_only if memoize_only
...@@ -1110,8 +1104,9 @@ class Repository ...@@ -1110,8 +1104,9 @@ class Repository
end end
instance_variable_set(ivar, value) instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to # Even if the above `#exists?` check passes these errors might still
# gracefully handle this and not cache anything. # occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything
fallback fallback
end end
end end
...@@ -1139,6 +1134,18 @@ class Repository ...@@ -1139,6 +1134,18 @@ class Repository
private private
# TODO Generice finder, later split this on finders by Ref or Oid
# gitlab-org/gitlab-ce#39239
def find_commit(oid_or_ref)
commit = if oid_or_ref.is_a?(Gitlab::Git::Commit)
oid_or_ref
else
Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
end
::Commit.new(commit, @project) if commit
end
def blob_data_at(sha, path) def blob_data_at(sha, path)
blob = blob_at(sha, path) blob = blob_at(sha, path)
return unless blob return unless blob
...@@ -1177,12 +1184,12 @@ class Repository ...@@ -1177,12 +1184,12 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path) def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c) commit_by(oid: c)
end end
def last_commit_for_path_by_rugged(sha, path) def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path)
commit(sha) commit_by(oid: sha)
end end
def last_commit_id_for_path_by_shelling_out(sha, path) def last_commit_id_for_path_by_shelling_out(sha, path)
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects Your projects
= nav_link(page: starred_dashboard_projects_path) do = nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
Starred projects Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do = link_to explore_root_path, data: {placement: 'right'} do
Explore projects Explore projects
.nav-controls .nav-controls
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ee', Gitlab::REVISION) %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ee', Gitlab::REVISION)
- if current_application_settings.version_check_enabled - if current_application_settings.version_check_enabled
= version_status_badge = version_status_badge
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
%br %br
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br %br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%hr %hr
.row.prepend-top-default .row.prepend-top-default
......
- page_title 'Instance Configuration'
.wiki.documentation
%h1 Instance Configuration
%p
In this page you will find information about the settings that are used in your current instance.
= render 'help/instance_configuration/ssh_info'
= render 'help/instance_configuration/gitlab_pages'
= render 'help/instance_configuration/gitlab_ci'
%p
%strong Table of contents
%ul
= content_for :table_content
= content_for :settings_content
- content_for :table_content do
%li= link_to 'GitLab CI', '#gitlab-ci'
- content_for :settings_content do
%h2#gitlab-ci
GitLab CI
%p
Below are the current settings regarding
= succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
.table-responsive
%table
%thead
%tr
%th Setting
%th= instance_configuration_host(@instance_configuration.settings[:host])
%th Default
%tbody
%tr
- artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
%td Artifacts maximum size
%td= instance_configuration_human_size_cell(artifacts_size[:value])
%td= instance_configuration_human_size_cell(artifacts_size[:default])
- gitlab_pages = @instance_configuration.settings[:gitlab_pages]
- content_for :table_content do
%li= link_to 'GitLab Pages', '#gitlab-pages'
- content_for :settings_content do
%h2#gitlab-pages
GitLab Pages
%p
Below are the settings for
= succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') }
.table-responsive
%table
%thead
%tr
%th Setting
%th= instance_configuration_host(@instance_configuration.settings[:host])
%tbody
%tr
%td Domain Name
%td
%code= instance_configuration_cell_html(gitlab_pages[:host])
%tr
%td IP Address
%td
%code= instance_configuration_cell_html(gitlab_pages[:ip_address])
%tr
%td Port
%td
%code= instance_configuration_cell_html(gitlab_pages[:port])
%br
%p
The maximum size of your Pages site is regulated by the artifacts maximum
size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }}
- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes]
- if ssh_info.any?
- content_for :table_content do
%li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints'
- content_for :settings_content do
%h2#ssh-host-keys-fingerprints
SSH host keys fingerprints
%p
Below are the fingerprints for the current instance SSH host keys.
.table-responsive
%table
%thead
%tr
%th Algorithm
%th MD5
%th SHA256
%tbody
- ssh_info.each do |algorithm|
%tr
%td= algorithm[:name]
%td
%code= instance_configuration_cell_html(algorithm[:md5])
%td
%code= instance_configuration_cell_html(algorithm[:sha256])
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
- if can?(current_user, :update_cluster, @cluster) - if can?(current_user, :update_cluster, @cluster)
.form-group .form-group
= field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success' = field.submit _('Save'), class: 'btn btn-success'
%section.settings#js-cluster-details %section.settings#js-cluster-details
.settings-header .settings-header
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
%section.settings#js-cluster-advanced-settings %section.settings#js-cluster-advanced-settings
.settings-header .settings-header
%h4= s_('ClusterIntegration|Advanced settings') %h4= _('Advanced settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
......
...@@ -77,5 +77,6 @@ ...@@ -77,5 +77,6 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
.mr-widget-pipeline-graph .mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
- if last_pipeline.duration
in in
= time_interval_in_words last_pipeline.duration = time_interval_in_words last_pipeline.duration
...@@ -72,6 +72,7 @@ ...@@ -72,6 +72,7 @@
%pre.light-well %pre.light-well
:preserve :preserve
cd existing_repo cd existing_repo
git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git push -u origin --all git push -u origin --all
git push -u origin --tags git push -u origin --tags
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- unless show_new_repo? - if show_new_repo?
.js-new-dropdown
- else
= render 'projects/tree/old_tree_header' = render 'projects/tree/old_tree_header'
.tree-controls .tree-controls
......
...@@ -29,7 +29,6 @@ ...@@ -29,7 +29,6 @@
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) } %input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } } %li.filter-dropdown-item{ data: { action: 'submit' } }
......
...@@ -7,4 +7,5 @@ ...@@ -7,4 +7,5 @@
blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'), blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }), new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s, can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
current_path: @path } }
---
title: Suggest to rename the remote for existing repository instructions
merge_request: 14970
author: helmo42
type: added
---
title: Add API endpoints for Pages Domains
merge_request: 13917
author: Travis Miller
type: added
---
title: Remove filter icon from search bar
merge_request:
author:
type: other
---
title: Case insensitive search for branches
merge_request: 14995
author: George Andrinopoulos
type: fixed
---
title: Moves placeholders components into shared folder with documentation. Makes
them easier to reuse in MR and Snippets comments
merge_request:
author:
type: other
---
title: Remove overzealous tooltips in projects page tabs
merge_request: 15017
author:
type: removed
---
title: Fix bitbucket login
merge_request: 15051
author:
type: fixed
---
title: Validate username/pw for Jiraservice, require them in the API
merge_request: 15025
author: Robert Schilling
type: fixed
---
title: Update the groups API documentation
merge_request: 15024
author: Robert Schilling
type: fixed
---
title: Don't rename paths that were freed up when upgrading
merge_request: 15029
author:
type: fixed
---
title: Automatic configuration settings page
merge_request: 13850
author: Francisco Lopez
type: added
---
title: Fix broken wiki pages that link to a wiki file
merge_request: 15019
author:
type: fixed
---
title: Hides pipeline duration in commit box when it is zero (nil)
merge_request: 14979
author: gvieira37
type: fixed
---
title: Auto Devops kubernetes default namespace is now correctly built out of gitlab
project group-name
merge_request: 14642
author: Mircea Danila Dumitrescu
type: fixed
---
title: Fix the writing of invalid environment refs
merge_request:
author:
type: fixed
---
title: Memoize GitLab logger to reduce open file descriptors
merge_request:
author:
type: fixed
---
title: Cache commits fetched from the repository
merge_request:
author:
type: performance
get 'help' => 'help#index' get 'help' => 'help#index'
get 'help/shortcuts' => 'help#shortcuts' get 'help/shortcuts' => 'help#shortcuts'
get 'help/ui' => 'help#ui' get 'help/ui' => 'help#ui'
get 'help/instance_configuration' => 'help#instance_configuration'
get 'help/*path' => 'help#show', as: :help_page get 'help/*path' => 'help#show', as: :help_page
...@@ -13,7 +13,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration ...@@ -13,7 +13,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
.well-known .well-known
abuse_reports abuse_reports
admin admin
all
api api
assets assets
autocomplete autocomplete
...@@ -24,29 +23,20 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration ...@@ -24,29 +23,20 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
groups groups
health_check health_check
help help
hooks
import import
invites invites
issues
jwt jwt
koding koding
member
merge_requests
new
notes
notification_settings notification_settings
oauth oauth
profile profile
projects projects
public public
repository
robots.txt robots.txt
s s
search search
sent_notifications sent_notifications
services
snippets snippets
teams
u u
unicorn_test unicorn_test
unsubscribes unsubscribes
...@@ -94,7 +84,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration ...@@ -94,7 +84,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
notification_setting notification_setting
pipeline_quota pipeline_quota
projects projects
subgroups
].freeze ].freeze
def up def up
......
...@@ -38,6 +38,7 @@ following locations: ...@@ -38,6 +38,7 @@ following locations:
- [Notes](notes.md) (comments) - [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md) - [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md) - [Open source license templates](templates/licenses.md)
- [Pages Domains](pages_domains.md)
- [Pipelines](pipelines.md) - [Pipelines](pipelines.md)
- [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Triggers](pipeline_triggers.md)
- [Pipeline Schedules](pipeline_schedules.md) - [Pipeline Schedules](pipeline_schedules.md)
......
...@@ -40,6 +40,38 @@ GET /groups ...@@ -40,6 +40,38 @@ GET /groups
] ]
``` ```
When adding the parameter `statistics=true` and the authenticated user is an admin, additional group statistics are returned.
```
GET /groups?statistics=true
```
```json
[
{
"id": 1,
"name": "Foobar Group",
"path": "foo-bar",
"description": "An interesting group",
"visibility": "public",
"lfs_enabled": true,
"avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
"web_url": "http://localhost:3000/groups/foo-bar",
"request_access_enabled": false,
"full_name": "Foobar Group",
"full_path": "foo-bar",
"parent_id": null,
"statistics": {
"storage_size" : 212,
"repository_size" : 33,
"lfs_objects_size" : 123,
"job_artifacts_size" : 57
}
}
]
```
You can search for groups by name or path, see below. You can search for groups by name or path, see below.
## List a group's projects ## List a group's projects
......
# Pages domains API
Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages](https://about.gitlab.com/features/pages/).
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
```http
GET /projects/:id/pages/domains
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
```json
[
{
"domain": "www.domain.example",
"url": "http://www.domain.example"
},
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
]
```
## Single pages domain
Get a single project pages domain. The user must have permissions to view pages domains.
```http
GET /projects/:id/pages/domains/:domain
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/www.domain.example
```
```json
{
"domain": "www.domain.example",
"url": "http://www.domain.example"
}
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
```
## Create new pages domain
Creates a new pages domain. The user must have permissions to create new pages domains.
```http
POST /projects/:id/pages/domains
```
| Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
| `key` | file/string | no | The certificate key in PEM format. |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
```
## Update pages domain
Updates an existing project pages domain. The user must have permissions to change an existing pages domains.
```http
PUT /projects/:id/pages/domains/:domain
```
| Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
| `key` | file/string | no | The certificate key in PEM format. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
"certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
"certificate_text": "Certificate:\n\n"
}
}
```
## Delete pages domain
Deletes an existing project pages domain.
```http
DELETE /projects/:id/pages/domains/:domain
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `domain` | string | yes | The domain |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
```
...@@ -478,8 +478,8 @@ PUT /projects/:id/services/jira ...@@ -478,8 +478,8 @@ PUT /projects/:id/services/jira
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `username` | string | yes | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | | `password` | string | yes | The password of the user created to be used with GitLab/JIRA. |
| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | | `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service ### Delete JIRA service
......
...@@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in ...@@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in. will be returned to GitLab and will be signed in.
## External Groups ## Marking Users as External based on SAML Groups
>**Note:** >**Note:**
This setting is only available on GitLab 8.7 and above. This setting is only available on GitLab 8.7 and above.
SAML login includes support for external groups. You can define in the SAML SAML login includes support for automatically identifying whether a user should
settings which groups, to which your users belong in your IdP, you wish to be be considered an [external](../user/permissions.md) user based on the user's group
marked as [external](../user/permissions.md). membership in the SAML identity provider. This feature **does not** allow you to
automatically add users to GitLab [Groups](../user/group/index.md), it simply
allows you to mark users as External if they are members of certain groups in the
Identity Provider.
### Requirements ### Requirements
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
1. [From ClearCase](clearcase.md) 1. [From ClearCase](clearcase.md)
1. [From CVS](cvs.md) 1. [From CVS](cvs.md)
1. [From FogBugz](fogbugz.md) 1. [From FogBugz](fogbugz.md)
1. [From GitHub.com of GitHub Enterprise](github.md) 1. [From GitHub.com or GitHub Enterprise](github.md)
1. [From GitLab.com](gitlab_com.md) 1. [From GitLab.com](gitlab_com.md)
1. [From Gitea](gitea.md) 1. [From Gitea](gitea.md)
1. [From Perforce](perforce.md) 1. [From Perforce](perforce.md)
......
...@@ -145,6 +145,7 @@ module API ...@@ -145,6 +145,7 @@ module API
mount ::API::Namespaces mount ::API::Namespaces
mount ::API::Notes mount ::API::Notes
mount ::API::NotificationSettings mount ::API::NotificationSettings
mount ::API::PagesDomains
mount ::API::Pipelines mount ::API::Pipelines
mount ::API::PipelineSchedules mount ::API::PipelineSchedules
mount ::API::ProjectHooks mount ::API::ProjectHooks
......
...@@ -1165,5 +1165,22 @@ module API ...@@ -1165,5 +1165,22 @@ module API
expose :key expose :key
expose :value expose :value
end end
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
expose :certificate
expose :certificate_text
end
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
end end
end end
...@@ -204,6 +204,10 @@ module API ...@@ -204,6 +204,10 @@ module API
end end
end end
def require_pages_enabled!
not_found! unless user_project.pages_available?
end
def can?(object, action, subject = :global) def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
......
module API
class PagesDomains < Grape::API
include PaginationParams
before do
authenticate!
require_pages_enabled!
end
after_validation do
normalize_params_file_to_string
end
helpers do
def find_pages_domain!
user_project.pages_domains.find_by(domain: params[:domain]) || not_found!('PagesDomain')
end
def pages_domain
@pages_domain ||= find_pages_domain!
end
def normalize_params_file_to_string
params.each do |k, v|
if v.is_a?(Hash) && v.key?(:tempfile)
params[k] = v[:tempfile].to_a.join('')
end
end
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all pages domains' do
success Entities::PagesDomain
end
params do
use :pagination
end
get ":id/pages/domains" do
authorize! :read_pages, user_project
present paginate(user_project.pages_domains.order(:domain)), with: Entities::PagesDomain
end
desc 'Get a single pages domain' do
success Entities::PagesDomain
end
params do
requires :domain, type: String, desc: 'The domain'
end
get ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
authorize! :read_pages, user_project
present pages_domain, with: Entities::PagesDomain
end
desc 'Create a new pages domain' do
success Entities::PagesDomain
end
params do
requires :domain, type: String, desc: 'The domain'
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
optional :key, allow_blank: false, types: [File, String], desc: 'The key'
all_or_none_of :certificate, :key
end
post ":id/pages/domains" do
authorize! :update_pages, user_project
pages_domain_params = declared(params, include_parent_namespaces: false)
pages_domain = user_project.pages_domains.create(pages_domain_params)
if pages_domain.persisted?
present pages_domain, with: Entities::PagesDomain
else
render_validation_error!(pages_domain)
end
end
desc 'Updates a pages domain'
params do
requires :domain, type: String, desc: 'The domain'
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
optional :key, allow_blank: false, types: [File, String], desc: 'The key'
end
put ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
authorize! :update_pages, user_project
pages_domain_params = declared(params, include_parent_namespaces: false)
# Remove empty private key if certificate is not empty.
if pages_domain_params[:certificate] && !pages_domain_params[:key]
pages_domain_params.delete(:key)
end
if pages_domain.update(pages_domain_params)
present pages_domain, with: Entities::PagesDomain
else
render_validation_error!(pages_domain)
end
end
desc 'Delete a pages domain'
params do
requires :domain, type: String, desc: 'The domain'
end
delete ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
authorize! :update_pages, user_project
status 204
pages_domain.destroy
end
end
end
end
...@@ -313,13 +313,13 @@ module API ...@@ -313,13 +313,13 @@ module API
desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
}, },
{ {
required: false, required: true,
name: :username, name: :username,
type: String, type: String,
desc: 'The username of the user created to be used with GitLab/JIRA' desc: 'The username of the user created to be used with GitLab/JIRA'
}, },
{ {
required: false, required: true,
name: :password, name: :password,
type: String, type: String,
desc: 'The password of the user created to be used with GitLab/JIRA' desc: 'The password of the user created to be used with GitLab/JIRA'
......
...@@ -72,7 +72,8 @@ module Gitlab ...@@ -72,7 +72,8 @@ module Gitlab
decorate(repo, commit) if commit decorate(repo, commit) if commit
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository,
Rugged::OdbError, Rugged::TreeError
nil nil
end end
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
def execute(pusher, repository, oldrev, newrev, ref) def execute(pusher, repository, oldrev, newrev, ref)
@repository = repository @repository = repository
@gl_id = pusher.gl_id @gl_id = pusher.gl_id
@gl_username = pusher.name @gl_username = pusher.username
@oldrev = oldrev @oldrev = oldrev
@newrev = newrev @newrev = newrev
@ref = ref @ref = ref
......
...@@ -166,7 +166,7 @@ module Gitlab ...@@ -166,7 +166,7 @@ module Gitlab
end end
def local_branches(sort_by: nil) def local_branches(sort_by: nil)
gitaly_migrate(:local_branches) do |is_enabled| gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by) gitaly_ref_client.local_branches(sort_by: sort_by)
else else
...@@ -749,6 +749,16 @@ module Gitlab ...@@ -749,6 +749,16 @@ module Gitlab
nil nil
end end
def ff_merge(user, source_sha, target_branch)
OperationService.new(user, self).with_branch(target_branch) do |our_commit|
raise ArgumentError, 'Invalid merge target' unless our_commit
source_sha
end
rescue Rugged::ReferenceError
raise ArgumentError, 'Invalid merge source'
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch( OperationService.new(user, self).with_branch(
branch_name, branch_name,
......
...@@ -7,9 +7,8 @@ module Gitlab ...@@ -7,9 +7,8 @@ module Gitlab
new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user)) new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user))
end end
# TODO support the username field in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/628
def self.from_gitaly(gitaly_user) def self.from_gitaly(gitaly_user)
new('', gitaly_user.name, gitaly_user.email, gitaly_user.gl_id) new(gitaly_user.gl_username, gitaly_user.name, gitaly_user.email, gitaly_user.gl_id)
end end
def initialize(username, name, email, gl_id) def initialize(username, name, email, gl_id)
...@@ -22,6 +21,10 @@ module Gitlab ...@@ -22,6 +21,10 @@ module Gitlab
def ==(other) def ==(other)
[username, name, email, gl_id] == [other.username, other.name, other.email, other.gl_id] [username, name, email, gl_id] == [other.username, other.name, other.email, other.gl_id]
end end
def to_gitaly
Gitaly::User.new(gl_username: username, gl_id: gl_id, name: name, email: email)
end
end end
end end
end end
...@@ -8,6 +8,7 @@ module Gitlab ...@@ -8,6 +8,7 @@ module Gitlab
{ name: name, email: email, message: message } { name: name, email: email, message: message }
end end
end end
PageBlob = Struct.new(:name)
def self.default_ref def self.default_ref
'master' 'master'
...@@ -80,7 +81,15 @@ module Gitlab ...@@ -80,7 +81,15 @@ module Gitlab
end end
def preview_slug(title, format) def preview_slug(title, format)
gollum_wiki.preview_page(title, '', format).url_path # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid
# using Rugged through a Gollum::Wiki instance
page_class = Gollum::Page
page = page_class.new(nil)
ext = page_class.format_to_ext(format.to_sym)
name = page_class.cname(title) + '.' + ext
blob = PageBlob.new(name)
page.populate(blob)
page.url_path
end end
private private
......
module Gitlab module Gitlab
module Git module Git
class WikiFile class WikiFile
attr_reader :mime_type, :raw_data, :name attr_reader :mime_type, :raw_data, :name, :path
# This class is meant to be serializable so that it can be constructed # This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab. # by Gitaly and sent over the network to GitLab.
...@@ -13,6 +13,7 @@ module Gitlab ...@@ -13,6 +13,7 @@ module Gitlab
@mime_type = gollum_file.mime_type @mime_type = gollum_file.mime_type
@raw_data = gollum_file.raw_data @raw_data = gollum_file.raw_data
@name = gollum_file.name @name = gollum_file.name
@path = gollum_file.path
end end
end end
end end
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
request = Gitaly::UserDeleteTagRequest.new( request = Gitaly::UserDeleteTagRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
tag_name: GitalyClient.encode(tag_name), tag_name: GitalyClient.encode(tag_name),
user: Util.gitaly_user(user) user: Gitlab::Git::User.from_gitlab(user).to_gitaly
) )
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request) response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request)
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
def add_tag(tag_name, user, target, message) def add_tag(tag_name, user, target, message)
request = Gitaly::UserCreateTagRequest.new( request = Gitaly::UserCreateTagRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
user: Util.gitaly_user(user), user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
tag_name: GitalyClient.encode(tag_name), tag_name: GitalyClient.encode(tag_name),
target_revision: GitalyClient.encode(target), target_revision: GitalyClient.encode(target),
message: GitalyClient.encode(message.to_s) message: GitalyClient.encode(message.to_s)
...@@ -45,7 +45,7 @@ module Gitlab ...@@ -45,7 +45,7 @@ module Gitlab
request = Gitaly::UserCreateBranchRequest.new( request = Gitaly::UserCreateBranchRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name), branch_name: GitalyClient.encode(branch_name),
user: Util.gitaly_user(user), user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
start_point: GitalyClient.encode(start_point) start_point: GitalyClient.encode(start_point)
) )
response = GitalyClient.call(@repository.storage, :operation_service, response = GitalyClient.call(@repository.storage, :operation_service,
...@@ -65,7 +65,7 @@ module Gitlab ...@@ -65,7 +65,7 @@ module Gitlab
request = Gitaly::UserDeleteBranchRequest.new( request = Gitaly::UserDeleteBranchRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name), branch_name: GitalyClient.encode(branch_name),
user: Util.gitaly_user(user) user: Gitlab::Git::User.from_gitlab(user).to_gitaly
) )
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request) response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request)
...@@ -87,7 +87,7 @@ module Gitlab ...@@ -87,7 +87,7 @@ module Gitlab
request_enum.push( request_enum.push(
Gitaly::UserMergeBranchRequest.new( Gitaly::UserMergeBranchRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
user: Util.gitaly_user(user), user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha, commit_id: source_sha,
branch: GitalyClient.encode(target_branch), branch: GitalyClient.encode(target_branch),
message: GitalyClient.encode(message) message: GitalyClient.encode(message)
......
...@@ -18,16 +18,6 @@ module Gitlab ...@@ -18,16 +18,6 @@ module Gitlab
) )
end end
def gitaly_user(gitlab_user)
return unless gitlab_user
Gitaly::User.new(
gl_id: Gitlab::GlId.gl_id(gitlab_user),
name: GitalyClient.encode(gitlab_user.name),
email: GitalyClient.encode(gitlab_user.email)
)
end
def gitlab_tag_from_gitaly_tag(repository, gitaly_tag) def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
if gitaly_tag.target_commit.present? if gitaly_tag.target_commit.present?
commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit) commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
end end
def self.read_latest def self.read_latest
path = Rails.root.join("log", file_name) path = self.full_log_path
return [] unless File.readable?(path) return [] unless File.readable?(path)
...@@ -22,7 +22,15 @@ module Gitlab ...@@ -22,7 +22,15 @@ module Gitlab
end end
def self.build def self.build
new(Rails.root.join("log", file_name)) RequestStore[self.cache_key] ||= new(self.full_log_path)
end
def self.full_log_path
Rails.root.join("log", file_name)
end
def self.cache_key
'logger:'.freeze + self.full_log_path.to_s
end end
end end
end end
...@@ -36,8 +36,8 @@ module Gitlab ...@@ -36,8 +36,8 @@ module Gitlab
end end
def track_query(raw_query, bindings, start, finish) def track_query(raw_query, bindings, start, finish)
query = Gitlab::Sherlock::Query.new(raw_query, start, finish) duration = finish - start
query_info = { duration: query.duration.round(3), sql: query.formatted_query } query_info = { duration: duration.round(3), sql: raw_query }
PEEK_DB_CLIENT.query_details << query_info PEEK_DB_CLIENT.query_details << query_info
end end
......
...@@ -36,6 +36,10 @@ module OmniAuth ...@@ -36,6 +36,10 @@ module OmniAuth
email_response = access_token.get('api/2.0/user/emails').parsed email_response = access_token.get('api/2.0/user/emails').parsed
@emails ||= email_response && email_response['values'] || nil @emails ||= email_response && email_response['values'] || nil
end end
def callback_url
options[:redirect_uri] || (full_host + script_name + callback_path)
end
end end
end end
end end
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-18 11:13+0200\n" "POT-Creation-Date: 2017-10-22 16:40+0300\n"
"PO-Revision-Date: 2017-10-18 11:13+0200\n" "PO-Revision-Date: 2017-10-22 16:40+0300\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -115,6 +115,9 @@ msgstr "" ...@@ -115,6 +115,9 @@ msgstr ""
msgid "AdminHealthPageLink|health page" msgid "AdminHealthPageLink|health page"
msgstr "" msgstr ""
msgid "Advanced settings"
msgstr ""
msgid "All" msgid "All"
msgstr "" msgstr ""
...@@ -561,7 +564,7 @@ msgstr "" ...@@ -561,7 +564,7 @@ msgstr ""
msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project." msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
msgstr "" msgstr ""
msgid "ClusterIntegration|Save" msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster" msgid "ClusterIntegration|See and edit the details for your cluster"
...@@ -1694,6 +1697,9 @@ msgstr "" ...@@ -1694,6 +1697,9 @@ msgstr ""
msgid "SSH Keys" msgid "SSH Keys"
msgstr "" msgstr ""
msgid "Save"
msgstr ""
msgid "Save changes" msgid "Save changes"
msgstr "" msgstr ""
......
...@@ -2,10 +2,6 @@ module QA ...@@ -2,10 +2,6 @@ module QA
module Page module Page
module Group module Group
class Show < Page::Base class Show < Page::Base
def go_to_subgroups
click_link 'Subgroups'
end
def go_to_subgroup(name) def go_to_subgroup(name)
click_link name click_link name
end end
...@@ -15,11 +11,19 @@ module QA ...@@ -15,11 +11,19 @@ module QA
end end
def go_to_new_subgroup def go_to_new_subgroup
click_on 'New Subgroup' within '.new-project-subgroup' do
find('.dropdown-toggle').click
find("li[data-value='new-subgroup']").click
end
find("input[data-action='new-subgroup']").click
end end
def go_to_new_project def go_to_new_project
click_on 'New Project' within '.new-project-subgroup' do
find('.dropdown-toggle').click
find("li[data-value='new-project']").click
end
find("input[data-action='new-project']").click
end end
end end
end end
......
...@@ -15,8 +15,6 @@ module QA ...@@ -15,8 +15,6 @@ module QA
Scenario::Gitlab::Sandbox::Prepare.perform Scenario::Gitlab::Sandbox::Prepare.perform
Page::Group::Show.perform do |page| Page::Group::Show.perform do |page|
page.go_to_subgroups
if page.has_subgroup?(Runtime::Namespace.name) if page.has_subgroup?(Runtime::Namespace.name)
page.go_to_subgroup(Runtime::Namespace.name) page.go_to_subgroup(Runtime::Namespace.name)
else else
......
...@@ -46,7 +46,7 @@ describe Projects::PipelinesController do ...@@ -46,7 +46,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do it 'limits the Gitaly requests' do
expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10) expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8)
end end
end end
end end
......
FactoryGirl.define do
factory :instance_configuration do
skip_create
end
end
...@@ -38,6 +38,8 @@ FactoryGirl.define do ...@@ -38,6 +38,8 @@ FactoryGirl.define do
active true active true
properties( properties(
url: 'https://jira.example.com', url: 'https://jira.example.com',
username: 'jira_user',
password: 'my-secret-password',
project_key: 'jira-key' project_key: 'jira-key'
) )
end end
......
require 'spec_helper'
feature 'Multi-file editor new directory', :js do
include WaitForRequests
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
project.add_master(user)
sign_in(user)
page.driver.set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
wait_for_requests
end
it 'creates directory in current directory' do
find('.add-to-tree').click
click_link('New directory')
page.within('.popup-dialog') do
find('.form-control').set('foldername')
click_button('Create directory')
end
fill_in('commit-message', with: 'commit message')
click_button('Commit 1 file')
expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
click_link('foldername')
expect(page).to have_selector('td', text: 'commit message', count: 2)
expect(page).to have_selector('td', text: '.gitkeep')
end
end
require 'spec_helper'
feature 'Multi-file editor new file', :js do
include WaitForRequests
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
project.add_master(user)
sign_in(user)
page.driver.set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
wait_for_requests
end
it 'creates file in current directory' do
find('.add-to-tree').click
click_link('New file')
page.within('.popup-dialog') do
find('.form-control').set('filename')
click_button('Create file')
end
find('.inputarea').send_keys('file content')
fill_in('commit-message', with: 'commit message')
click_button('Commit 1 file')
expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
end
end
...@@ -46,6 +46,15 @@ describe BranchesFinder do ...@@ -46,6 +46,15 @@ describe BranchesFinder do
expect(result.count).to eq(1) expect(result.count).to eq(1)
end end
it 'filters branches by name ignoring letter case' do
branches_finder = described_class.new(repository, { search: 'FiX' })
result = branches_finder.execute
expect(result.first.name).to eq('fix')
expect(result.count).to eq(1)
end
it 'does not find any branch with that name' do it 'does not find any branch with that name' do
branches_finder = described_class.new(repository, { search: 'random' }) branches_finder = described_class.new(repository, { search: 'random' })
......
{
"type": "array",
"items": {
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"expired": { "type": "boolean" },
"certificate": { "type": "string" },
"certificate_text": { "type": "string" }
},
"required": ["subject", "expired"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
}
require 'spec_helper'
describe InstanceConfigurationHelper do
describe '#instance_configuration_cell_html' do
describe 'if not block is passed' do
it 'returns the parameter if present' do
expect(helper.instance_configuration_cell_html('gitlab')).to eq('gitlab')
end
it 'returns "-" if the parameter is blank' do
expect(helper.instance_configuration_cell_html(nil)).to eq('-')
expect(helper.instance_configuration_cell_html('')).to eq('-')
end
end
describe 'if a block is passed' do
let(:upcase_block) { ->(value) { value.upcase } }
it 'returns the result of the block' do
expect(helper.instance_configuration_cell_html('gitlab', &upcase_block)).to eq('GITLAB')
expect(helper.instance_configuration_cell_html('gitlab') { |v| v.upcase }).to eq('GITLAB')
end
it 'returns "-" if the parameter is blank' do
expect(helper.instance_configuration_cell_html(nil, &upcase_block)).to eq('-')
expect(helper.instance_configuration_cell_html(nil) { |v| v.upcase }).to eq('-')
expect(helper.instance_configuration_cell_html('', &upcase_block)).to eq('-')
end
end
it 'boolean are valid values to display' do
expect(helper.instance_configuration_cell_html(true)).to eq(true)
expect(helper.instance_configuration_cell_html(false)).to eq(false)
end
end
describe '#instance_configuration_human_size_cell' do
it 'returns "-" if the parameter is blank' do
expect(helper.instance_configuration_human_size_cell(nil)).to eq('-')
expect(helper.instance_configuration_human_size_cell('')).to eq('-')
end
it 'accepts the value in bytes' do
expect(helper.instance_configuration_human_size_cell(1024)).to eq('1 KB')
end
it 'returns the value in human size readable format' do
expect(helper.instance_configuration_human_size_cell(1048576)).to eq('1 MB')
end
end
end
export default (Component, props = {}) => new Component({ export default (Component, props = {}, el = null) => new Component({
propsData: props, propsData: props,
}).$mount(); }).$mount(el);
import Vue from 'vue';
import newDropdown from '~/repo/components/new_dropdown/index.vue';
import RepoStore from '~/repo/stores/repo_store';
import RepoHelper from '~/repo/helpers/repo_helper';
import eventHub from '~/repo/event_hub';
import createComponent from '../../../helpers/vue_mount_component_helper';
describe('new dropdown component', () => {
let vm;
beforeEach(() => {
const component = Vue.extend(newDropdown);
vm = createComponent(component);
});
afterEach(() => {
vm.$destroy();
RepoStore.files = [];
RepoStore.openedFiles = [];
RepoStore.setViewToPreview();
});
it('renders new file and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
it('sets modalType to blob when new file is clicked', () => {
vm.$el.querySelectorAll('a')[0].click();
expect(vm.modalType).toBe('blob');
});
it('sets modalType to tree when new directory is clicked', () => {
vm.$el.querySelectorAll('a')[1].click();
expect(vm.modalType).toBe('tree');
});
it('opens modal when link is clicked', (done) => {
vm.$el.querySelectorAll('a')[0].click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.modal')).not.toBeNull();
done();
});
});
});
describe('toggleModalOpen', () => {
it('closes modal after toggling', (done) => {
vm.toggleModalOpen();
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.modal')).not.toBeNull();
})
.then(vm.toggleModalOpen)
.then(() => {
expect(vm.$el.querySelector('.modal')).toBeNull();
})
.then(done)
.catch(done.fail);
});
});
describe('createEntryInStore', () => {
['tree', 'blob'].forEach((type) => {
describe(type, () => {
it('closes modal after creating file', () => {
vm.openModal = true;
eventHub.$emit('createNewEntry', 'testing', type);
expect(vm.openModal).toBeFalsy();
});
it('sets editMode to true', () => {
eventHub.$emit('createNewEntry', 'testing', type);
expect(RepoStore.editMode).toBeTruthy();
});
it('toggles blob view', () => {
eventHub.$emit('createNewEntry', 'testing', type);
expect(RepoStore.isPreviewView()).toBeFalsy();
});
it('adds file into activeFiles', () => {
eventHub.$emit('createNewEntry', 'testing', type);
expect(RepoStore.openedFiles.length).toBe(1);
});
it(`creates ${type} in the current stores path`, () => {
RepoStore.path = 'testing';
eventHub.$emit('createNewEntry', 'testing/app', type);
expect(RepoStore.files[0].path).toBe('testing/app');
expect(RepoStore.files[0].name).toBe('app');
if (type === 'tree') {
expect(RepoStore.files[0].files.length).toBe(1);
}
RepoStore.path = '';
});
});
});
describe('file', () => {
it('creates new file', () => {
eventHub.$emit('createNewEntry', 'testing', 'blob');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
expect(RepoStore.files[0].type).toBe('blob');
expect(RepoStore.files[0].tempFile).toBeTruthy();
});
it('does not create temp file when file already exists', () => {
RepoStore.files.push(RepoHelper.serializeRepoEntity('blob', {
name: 'testing',
}));
eventHub.$emit('createNewEntry', 'testing', 'blob');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
expect(RepoStore.files[0].type).toBe('blob');
expect(RepoStore.files[0].tempFile).toBeUndefined();
});
});
describe('tree', () => {
it('creates new tree', () => {
eventHub.$emit('createNewEntry', 'testing', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('testing');
expect(RepoStore.files[0].type).toBe('tree');
expect(RepoStore.files[0].tempFile).toBeTruthy();
expect(RepoStore.files[0].files.length).toBe(1);
expect(RepoStore.files[0].files[0].name).toBe('.gitkeep');
});
it('creates multiple trees when entryName has slashes', () => {
eventHub.$emit('createNewEntry', 'app/test', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
expect(RepoStore.files[0].files[0].name).toBe('test');
expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
});
it('creates tree in existing tree', () => {
RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
name: 'app',
}));
eventHub.$emit('createNewEntry', 'app/test', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
expect(RepoStore.files[0].tempFile).toBeUndefined();
expect(RepoStore.files[0].files[0].tempFile).toBeTruthy();
expect(RepoStore.files[0].files[0].name).toBe('test');
expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
});
it('does not create new tree when already exists', () => {
RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
name: 'app',
}));
eventHub.$emit('createNewEntry', 'app', 'tree');
expect(RepoStore.files.length).toBe(1);
expect(RepoStore.files[0].name).toBe('app');
expect(RepoStore.files[0].tempFile).toBeUndefined();
expect(RepoStore.files[0].files.length).toBe(0);
});
});
});
});
import Vue from 'vue';
import RepoStore from '~/repo/stores/repo_store';
import modal from '~/repo/components/new_dropdown/modal.vue';
import eventHub from '~/repo/event_hub';
import createComponent from '../../../helpers/vue_mount_component_helper';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
let vm;
afterEach(() => {
vm.$destroy();
RepoStore.files = [];
RepoStore.openedFiles = [];
RepoStore.setViewToPreview();
});
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
vm = createComponent(Component, {
type,
currentPath: RepoStore.path,
});
});
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
const title = type === 'tree' ? 'Directory' : 'File';
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
});
});
});
it('focuses field on mount', () => {
document.body.innerHTML += '<div class="js-test"></div>';
vm = createComponent(Component, {
type: 'tree',
currentPath: RepoStore.path,
}, '.js-test');
expect(document.activeElement).toBe(vm.$refs.fieldName);
vm.$el.remove();
});
describe('createEntryInStore', () => {
it('emits createNewEntry event', () => {
spyOn(eventHub, '$emit');
vm = createComponent(Component, {
type: 'tree',
currentPath: RepoStore.path,
});
vm.entryName = 'testing';
vm.createEntryInStore();
expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree');
});
});
});
...@@ -3,6 +3,15 @@ import repoFileButtons from '~/repo/components/repo_file_buttons.vue'; ...@@ -3,6 +3,15 @@ import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
describe('RepoFileButtons', () => { describe('RepoFileButtons', () => {
const activeFile = {
extension: 'md',
url: 'url',
raw_path: 'raw_path',
blame_path: 'blame_path',
commits_path: 'commits_path',
permalink: 'permalink',
};
function createComponent() { function createComponent() {
const RepoFileButtons = Vue.extend(repoFileButtons); const RepoFileButtons = Vue.extend(repoFileButtons);
...@@ -14,14 +23,6 @@ describe('RepoFileButtons', () => { ...@@ -14,14 +23,6 @@ describe('RepoFileButtons', () => {
}); });
it('renders Raw, Blame, History, Permalink and Preview toggle', () => { it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
const activeFile = {
extension: 'md',
url: 'url',
raw_path: 'raw_path',
blame_path: 'blame_path',
commits_path: 'commits_path',
permalink: 'permalink',
};
const activeFileLabel = 'activeFileLabel'; const activeFileLabel = 'activeFileLabel';
RepoStore.openedFiles = new Array(1); RepoStore.openedFiles = new Array(1);
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
...@@ -34,7 +35,6 @@ describe('RepoFileButtons', () => { ...@@ -34,7 +35,6 @@ describe('RepoFileButtons', () => {
const blame = vm.$el.querySelector('.blame'); const blame = vm.$el.querySelector('.blame');
const history = vm.$el.querySelector('.history'); const history = vm.$el.querySelector('.history');
expect(vm.$el.id).toEqual('repo-file-buttons');
expect(raw.href).toMatch(`/${activeFile.raw_path}`); expect(raw.href).toMatch(`/${activeFile.raw_path}`);
expect(raw.textContent.trim()).toEqual('Raw'); expect(raw.textContent.trim()).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blame_path}`); expect(blame.href).toMatch(`/${activeFile.blame_path}`);
...@@ -46,10 +46,6 @@ describe('RepoFileButtons', () => { ...@@ -46,10 +46,6 @@ describe('RepoFileButtons', () => {
}); });
it('triggers rawPreviewToggle on preview click', () => { it('triggers rawPreviewToggle on preview click', () => {
const activeFile = {
extension: 'md',
url: 'url',
};
RepoStore.openedFiles = new Array(1); RepoStore.openedFiles = new Array(1);
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
RepoStore.editMode = true; RepoStore.editMode = true;
...@@ -65,10 +61,7 @@ describe('RepoFileButtons', () => { ...@@ -65,10 +61,7 @@ describe('RepoFileButtons', () => {
}); });
it('does not render preview toggle if not canPreview', () => { it('does not render preview toggle if not canPreview', () => {
const activeFile = { activeFile.extension = 'js';
extension: 'abcd',
url: 'url',
};
RepoStore.openedFiles = new Array(1); RepoStore.openedFiles = new Array(1);
RepoStore.activeFile = activeFile; RepoStore.activeFile = activeFile;
......
...@@ -7,6 +7,7 @@ import { file } from '../mock_data'; ...@@ -7,6 +7,7 @@ import { file } from '../mock_data';
describe('RepoFile', () => { describe('RepoFile', () => {
const updated = 'updated'; const updated = 'updated';
const otherFile = { const otherFile = {
id: 'test',
html: '<p class="file-content">html</p>', html: '<p class="file-content">html</p>',
pageTitle: 'otherpageTitle', pageTitle: 'otherpageTitle',
}; };
......
import Vue from 'vue'; import Vue from 'vue';
import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue'; import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
import { userDataMock } from '../mock_data'; import { userDataMock } from '../../../notes/mock_data';
describe('issue placeholder system note component', () => { describe('issue placeholder system note component', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue'; import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
let vm;
describe('issue placeholder system note component', () => {
let mountComponent;
beforeEach(() => { beforeEach(() => {
const PlaceholderSystemNote = Vue.extend(placeholderSystemNote); PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
});
mountComponent = props => new PlaceholderSystemNote({ afterEach(() => {
propsData: { vm.$destroy();
note: {
body: props,
},
},
}).$mount();
}); });
it('should render system note placeholder with plain text', () => { it('should render system note placeholder with plain text', () => {
const vm = mountComponent('This is a placeholder'); vm = mountComponent(PlaceholderSystemNote, {
note: { body: 'This is a placeholder' },
});
expect(vm.$el.tagName).toEqual('LI'); expect(vm.$el.tagName).toEqual('LI');
expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder');
......
import Vue from 'vue'; import Vue from 'vue';
import issueSystemNote from '~/notes/components/issue_system_note.vue'; import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import store from '~/notes/stores'; import store from '~/notes/stores';
describe('issue system note', () => { describe('issue system note', () => {
...@@ -33,6 +33,10 @@ describe('issue system note', () => { ...@@ -33,6 +33,10 @@ describe('issue system note', () => {
}).$mount(); }).$mount();
}); });
afterEach(() => {
vm.$destroy();
});
it('should render a list item with correct id', () => { it('should render a list item with correct id', () => {
expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`);
}); });
......
...@@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () { ...@@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () {
imgCssClasses: 'myextraavatarclass', imgCssClasses: 'myextraavatarclass',
tooltipText: 'tooltip text', tooltipText: 'tooltip text',
tooltipPlacement: 'bottom', tooltipPlacement: 'bottom',
username: 'username',
}; };
const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
...@@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () { ...@@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () {
expect(this.userAvatarLink[key]).toBeDefined(); expect(this.userAvatarLink[key]).toBeDefined();
}); });
}); });
describe('no username', function () {
beforeEach(function (done) {
this.userAvatarLink.username = '';
Vue.nextTick(done);
});
it('should only render image tag in link', function () {
const childElements = this.userAvatarLink.$el.childNodes;
expect(childElements[0].tagName).toBe('IMG');
// Vue will render the hidden component as <!---->
expect(childElements[1].tagName).toBeUndefined();
});
it('should render avatar image tooltip', function () {
expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText);
});
});
describe('username', function () {
it('should not render avatar image tooltip', function () {
expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
});
it('should render username prop in <span>', function () {
expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username);
});
it('should render text tooltip for <span>', function () {
expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText);
});
it('should render text tooltip placement for <span>', function () {
expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement);
});
});
}); });
...@@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do ...@@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do
context 'linking internal images' do context 'linking internal images' do
it 'creates img tag if image exists' do it 'creates img tag if image exists' do
file = Gollum::File.new(project_wiki.wiki) gollum_file_double = double('Gollum::File',
expect(file).to receive(:path).and_return('images/image.jpg') mime_type: 'image/jpeg',
expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file) name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(wiki_file)
tag = '[[images/image.jpg]]' tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
......
require 'spec_helper'
describe Gitlab::AppLogger, :request_store do
subject { described_class }
it 'builds a logger once' do
expect(::Logger).to receive(:new).and_call_original
subject.info('hello world')
subject.error('hello again')
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Git::HooksService, seed_helper: true do describe Gitlab::Git::HooksService, seed_helper: true do
let(:user) { Gitlab::Git::User.new('janedoe', 'Jane Doe', 'janedoe@example.com', 'user-456') } let(:gl_id) { 'user-456' }
let(:gl_username) { 'janedoe' }
let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) }
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') }
let(:service) { described_class.new } let(:service) { described_class.new }
let(:blankrev) { Gitlab::Git::BLANK_SHA }
before do let(:oldrev) { SeedRepo::Commit::PARENT_ID }
@blankrev = Gitlab::Git::BLANK_SHA let(:newrev) { SeedRepo::Commit::ID }
@oldrev = SeedRepo::Commit::PARENT_ID let(:ref) { 'refs/heads/feature' }
@newrev = SeedRepo::Commit::ID
@ref = 'refs/heads/feature'
end
describe '#execute' do describe '#execute' do
context 'when receive hooks were successful' do context 'when receive hooks were successful' do
it 'calls post-receive hook' do let(:hook) { double(:hook) }
hook = double(trigger: [true, nil])
it 'calls all three hooks' do
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref)
.exactly(3).times.and_return([true, nil])
service.execute(user, repository, @blankrev, @newrev, @ref) { } service.execute(user, repository, blankrev, newrev, ref) { }
end end
end end
...@@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do ...@@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive') expect(service).not_to receive(:run_hook).with('post-receive')
expect do expect do
service.execute(user, repository, @blankrev, @newrev, @ref) service.execute(user, repository, blankrev, newrev, ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end end
end end
...@@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do ...@@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive') expect(service).not_to receive(:run_hook).with('post-receive')
expect do expect do
service.execute(user, repository, @blankrev, @newrev, @ref) service.execute(user, repository, blankrev, newrev, ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end end
end end
......
...@@ -1590,6 +1590,60 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1590,6 +1590,60 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
describe '#ff_merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:user) { build(:user) }
let(:target_branch) { 'test-ff-target-branch' }
before do
repository.create_branch(target_branch, branch_head)
end
after do
ensure_seeds
end
subject { repository.ff_merge(user, source_sha, target_branch) }
it 'performs a ff_merge' do
expect(subject.newrev).to eq(source_sha)
expect(subject.repo_created).to be(false)
expect(subject.branch_created).to be(false)
expect(repository.commit(target_branch).id).to eq(source_sha)
end
context 'with a non-existing target branch' do
subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
it 'throws an ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
end
end
context 'with a non-existing source commit' do
let(:source_sha) { 'f001' }
it 'throws an ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
end
end
context 'when the source sha is not a descendant of the branch head' do
let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
it "doesn't perform the ff_merge" do
expect { subject }.to raise_error(Gitlab::Git::CommitError)
expect(repository.commit(target_branch).id).to eq(branch_head)
end
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name) def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name } source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged rugged = repository.rugged
......
...@@ -5,14 +5,20 @@ describe Gitlab::Git::User do ...@@ -5,14 +5,20 @@ describe Gitlab::Git::User do
let(:name) { 'Jane Doe' } let(:name) { 'Jane Doe' }
let(:email) { 'janedoe@example.com' } let(:email) { 'janedoe@example.com' }
let(:gl_id) { 'user-123' } let(:gl_id) { 'user-123' }
let(:user) do
described_class.new(username, name, email, gl_id)
end
subject { described_class.new(username, name, email, gl_id) } subject { described_class.new(username, name, email, gl_id) }
describe '.from_gitaly' do describe '.from_gitaly' do
let(:gitaly_user) { Gitaly::User.new(name: name, email: email, gl_id: gl_id) } let(:gitaly_user) do
Gitaly::User.new(gl_username: username, name: name, email: email, gl_id: gl_id)
end
subject { described_class.from_gitaly(gitaly_user) } subject { described_class.from_gitaly(gitaly_user) }
it { expect(subject).to eq(described_class.new('', name, email, gl_id)) } it { expect(subject).to eq(user) }
end end
describe '.from_gitlab' do describe '.from_gitlab' do
...@@ -35,4 +41,16 @@ describe Gitlab::Git::User do ...@@ -35,4 +41,16 @@ describe Gitlab::Git::User do
it { expect(subject).not_to eq_other(username, name, email + 'x', gl_id) } it { expect(subject).not_to eq_other(username, name, email + 'x', gl_id) }
it { expect(subject).not_to eq_other(username, name, email, gl_id + 'x') } it { expect(subject).not_to eq_other(username, name, email, gl_id + 'x') }
end end
describe '#to_gitaly' do
subject { user.to_gitaly }
it 'creates a Gitaly::User with the correct data' do
expect(subject).to be_a(Gitaly::User)
expect(subject.gl_username).to eq(username)
expect(subject.name).to eq(name)
expect(subject.email).to eq(email)
expect(subject.gl_id).to eq(gl_id)
end
end
end end
...@@ -5,7 +5,7 @@ describe Gitlab::GitalyClient::OperationService do ...@@ -5,7 +5,7 @@ describe Gitlab::GitalyClient::OperationService do
let(:repository) { project.repository.raw } let(:repository) { project.repository.raw }
let(:client) { described_class.new(repository) } let(:client) { described_class.new(repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } let(:gitaly_user) { Gitlab::Git::User.from_gitlab(user).to_gitaly }
describe '#user_create_branch' do describe '#user_create_branch' do
let(:branch_name) { 'new' } let(:branch_name) { 'new' }
......
...@@ -26,18 +26,4 @@ describe Gitlab::GitalyClient::Util do ...@@ -26,18 +26,4 @@ describe Gitlab::GitalyClient::Util do
expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory) expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory)
end end
end end
describe '.gitaly_user' do
let(:user) { create(:user) }
let(:gl_id) { Gitlab::GlId.gl_id(user) }
subject { described_class.gitaly_user(user) }
it 'creates a Gitaly::User from a GitLab user' do
expect(subject).to be_a(Gitaly::User)
expect(subject.name).to eq(user.name)
expect(subject.email).to eq(user.email)
expect(subject.gl_id).to eq(gl_id)
end
end
end end
...@@ -663,6 +663,16 @@ describe Environment do ...@@ -663,6 +663,16 @@ describe Environment do
end end
end end
describe '#ref_path' do
subject(:environment) do
create(:environment, name: 'staging / review-1')
end
it 'returns a path that uses the slug and does not have spaces' do
expect(environment.ref_path).to start_with('refs/environments/staging-review-1-')
end
end
describe '#external_url_for' do describe '#external_url_for' do
let(:source_path) { 'source/file.html' } let(:source_path) { 'source/file.html' }
let(:sha) { RepoHelpers.sample_commit.id } let(:sha) { RepoHelpers.sample_commit.id }
......
require 'spec_helper'
RSpec.describe InstanceConfiguration do
context 'without cache' do
describe '#settings' do
describe '#ssh_algorithms_hashes' do
let(:md5) { '54:e0:f8:70:d6:4f:4c:b1:b3:02:44:77:cf:cd:0d:fc' }
let(:sha256) { '9327f0d15a48c4d9f6a3aee65a1825baf9a3412001c98169c5fd022ac27762fc' }
it 'does not return anything if file does not exist' do
stub_pub_file(exist: false)
expect(subject.settings[:ssh_algorithms_hashes]).to be_empty
end
it 'does not return anything if file is empty' do
stub_pub_file
allow(File).to receive(:read).and_return('')
expect(subject.settings[:ssh_algorithms_hashes]).to be_empty
end
it 'returns the md5 and sha256 if file valid and exists' do
stub_pub_file
result = subject.settings[:ssh_algorithms_hashes].select { |o| o[:md5] == md5 && o[:sha256] == sha256 }
expect(result.size).to eq(InstanceConfiguration::SSH_ALGORITHMS.size)
end
def stub_pub_file(exist: true)
path = 'spec/fixtures/ssh_host_example_key.pub'
path << 'random' unless exist
allow(subject).to receive(:ssh_algorithm_file).and_return(Rails.root.join(path))
end
end
describe '#host' do
it 'returns current instance host' do
allow(Settings.gitlab).to receive(:host).and_return('exampledomain')
expect(subject.settings[:host]).to eq(Settings.gitlab.host)
end
end
describe '#gitlab_pages' do
let(:gitlab_pages) { subject.settings[:gitlab_pages] }
it 'returns Settings.pages' do
gitlab_pages.delete(:ip_address)
expect(gitlab_pages).to eq(Settings.pages.symbolize_keys)
end
it 'returns the Gitlab\'s pages host ip address' do
expect(gitlab_pages.keys).to include(:ip_address)
end
it 'returns the ip address as nil if the domain is invalid' do
allow(Settings.pages).to receive(:host).and_return('exampledomain')
expect(gitlab_pages[:ip_address]).to eq nil
end
it 'returns the ip address of the domain' do
allow(Settings.pages).to receive(:host).and_return('localhost')
expect(gitlab_pages[:ip_address]).to eq('127.0.0.1').or eq('::1')
end
end
describe '#gitlab_ci' do
let(:gitlab_ci) { subject.settings[:gitlab_ci] }
it 'returns Settings.gitalb_ci' do
gitlab_ci.delete(:artifacts_max_size)
expect(gitlab_ci).to eq(Settings.gitlab_ci.symbolize_keys)
end
it 'returns the key artifacts_max_size' do
expect(gitlab_ci.keys).to include(:artifacts_max_size)
end
end
end
end
context 'with cache', :use_clean_rails_memory_store_caching do
it 'caches settings content' do
expect(Rails.cache.read(described_class::CACHE_KEY)).to be_nil
settings = subject.settings
expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(settings)
end
describe 'cached settings' do
before do
subject.settings
end
it 'expires after EXPIRATION_TIME' do
allow(Time).to receive(:now).and_return(Time.now + described_class::EXPIRATION_TIME)
Rails.cache.cleanup
expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(nil)
end
end
end
end
...@@ -86,7 +86,7 @@ describe MergeRequest do ...@@ -86,7 +86,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do context 'when the target branch does not exist' do
before do before do
project.repository.raw_repository.delete_branch(subject.target_branch) project.repository.rm_branch(subject.author, subject.target_branch)
end end
it 'returns nil' do it 'returns nil' do
...@@ -1815,7 +1815,7 @@ describe MergeRequest do ...@@ -1815,7 +1815,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do context 'when the target branch does not exist' do
before do before do
subject.project.repository.raw_repository.delete_branch(subject.target_branch) subject.project.repository.rm_branch(subject.author, subject.target_branch)
end end
it 'returns nil' do it 'returns nil' do
......
...@@ -24,6 +24,8 @@ describe JiraService do ...@@ -24,6 +24,8 @@ describe JiraService do
end end
it { is_expected.not_to validate_presence_of(:url) } it { is_expected.not_to validate_presence_of(:url) }
it { is_expected.not_to validate_presence_of(:username) }
it { is_expected.not_to validate_presence_of(:password) }
end end
context 'validating urls' do context 'validating urls' do
...@@ -54,6 +56,18 @@ describe JiraService do ...@@ -54,6 +56,18 @@ describe JiraService do
expect(service).not_to be_valid expect(service).not_to be_valid
end end
it 'is not valid when username is missing' do
service.username = nil
expect(service).not_to be_valid
end
it 'is not valid when password is missing' do
service.password = nil
expect(service).not_to be_valid
end
it 'is valid when api url is a valid url' do it 'is valid when api url is a valid url' do
service.api_url = 'http://jira.test.com/api' service.api_url = 'http://jira.test.com/api'
......
...@@ -99,45 +99,34 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do ...@@ -99,45 +99,34 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe '#actual_namespace' do describe '#actual_namespace' do
subject { service.actual_namespace } subject { service.actual_namespace }
it "returns the default namespace" do shared_examples 'a correctly formatted namespace' do
is_expected.to eq(service.send(:default_namespace)) it 'returns a valid Kubernetes namespace name' do
expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex)
expect(subject).to eq(expected_namespace)
end end
context 'when namespace is specified' do
before do
service.namespace = 'my-namespace'
end end
it "returns the user-namespace" do it_behaves_like 'a correctly formatted namespace' do
is_expected.to eq('my-namespace') let(:expected_namespace) { service.send(:default_namespace) }
end
end end
context 'when service is not assigned to project' do context 'when the project path contains forbidden characters' do
before do before do
service.project = nil project.path = '-a_Strange.Path--forSure'
end end
it "does not return namespace" do it_behaves_like 'a correctly formatted namespace' do
is_expected.to be_nil let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" }
end
end end
end end
describe '#actual_namespace' do
subject { service.actual_namespace }
it "returns the default namespace" do
is_expected.to eq(service.send(:default_namespace))
end
context 'when namespace is specified' do context 'when namespace is specified' do
before do before do
service.namespace = 'my-namespace' service.namespace = 'my-namespace'
end end
it "returns the user-namespace" do it_behaves_like 'a correctly formatted namespace' do
is_expected.to eq('my-namespace') let(:expected_namespace) { 'my-namespace' }
end end
end end
...@@ -146,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do ...@@ -146,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
service.project = nil service.project = nil
end end
it "does not return namespace" do it 'does not return namespace' do
is_expected.to be_nil is_expected.to be_nil
end end
end end
......
...@@ -2245,19 +2245,41 @@ describe Repository do ...@@ -2245,19 +2245,41 @@ describe Repository do
end end
describe '#cache_method_output', :use_clean_rails_memory_store_caching do describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
context 'with a non-existing repository' do context 'with a non-existing repository' do
let(:value) do let(:project) { create(:project) } # No repository
repository.cache_method_output(:cats, fallback: 10) do
raise Rugged::ReferenceError subject do
repository.cache_method_output(:cats, fallback: fallback) do
repository.cats_call_stub
end
end
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'avoids calling the original method' do
expect(repository).not_to receive(:cats_call_stub)
subject
end
end
context 'with a method throwing a non-existing-repository error' do
subject do
repository.cache_method_output(:cats, fallback: fallback) do
raise Gitlab::Git::Repository::NoRepository
end end
end end
it 'returns a fallback value' do it 'returns the fallback value' do
expect(value).to eq(10) expect(subject).to eq(fallback)
end end
it 'does not cache the data' do it 'does not cache the data' do
value subject
expect(repository.instance_variable_defined?(:@cats)).to eq(false) expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(repository.send(:cache).exist?(:cats)).to eq(false) expect(repository.send(:cache).exist?(:cats)).to eq(false)
...@@ -2388,4 +2410,24 @@ describe Repository do ...@@ -2388,4 +2410,24 @@ describe Repository do
end end
end end
end end
describe 'commit cache' do
set(:project) { create(:project, :repository) }
it 'caches based on SHA' do
# Gets the commit oid, and warms the cache
oid = project.commit.id
expect(Gitlab::Git::Commit).not_to receive(:find).once
project.commit_by(oid: oid)
end
it 'caches nil values' do
expect(Gitlab::Git::Commit).to receive(:find).once
project.commit_by(oid: '1' * 40)
project.commit_by(oid: '1' * 40)
end
end
end end
require 'rails_helper'
describe API::PagesDomains do
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, :with_key, domain: 'expired.domain.test', project: project) }
let(:pages_domain_params) { build(:pages_domain, domain: 'www.other-domain.test').slice(:domain) }
let(:pages_domain_secure_params) { build(:pages_domain, :with_certificate, :with_key, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, :with_key, project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) }
let(:route) { "/projects/#{project.id}/pages/domains" }
let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" }
let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" }
let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" }
let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
end
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
it 'returns paginated pages domains' do
get api(route, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.map { |pages_domain| pages_domain['domain'] }).to include(pages_domain.domain)
expect(json_response.last).to have_key('domain')
end
end
context 'when pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'get pages domains'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
end
end
end
describe 'GET /projects/:project_id/pages/domains/:domain' do
shared_examples_for 'get pages domain' do
it 'returns pages domain' do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['domain']).to eq(pages_domain.domain)
expect(json_response['url']).to eq(pages_domain.url)
expect(json_response['certificate']).to be_nil
end
it 'returns pages domain with a certificate' do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['domain']).to eq(pages_domain_secure.domain)
expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
expect(json_response['certificate']['expired']).to be false
end
it 'returns pages domain with an expired certificate' do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['certificate']['expired']).to be true
end
end
context 'when domain is vacant' do
before do
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { get api(route_vacant_domain, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'get pages domain'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
end
end
end
describe 'POST /projects/:project_id/pages/domains' do
let(:params) { pages_domain_params.slice(:domain) }
let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) }
shared_examples_for 'post pages domains' do
it 'creates a new pages domain' do
post api(route, user), params
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil
end
it 'creates a new secure pages domain' do
post api(route, user), params_secure
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
it 'fails to create pages domain without key' do
post api(route, user), pages_domain_secure_params.slice(:domain, :certificate)
expect(response).to have_gitlab_http_status(400)
end
it 'fails to create pages domain with key missmatch' do
post api(route, user), pages_domain_secure_key_missmatch_params.slice(:domain, :certificate, :key)
expect(response).to have_gitlab_http_status(400)
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'post pages domains'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { post api(route, user), params }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { post api(route, user), params }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { post api(route, user), params }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { post api(route, user), params }
end
end
end
describe 'PUT /projects/:project_id/pages/domains/:domain' do
let(:params_secure) { pages_domain_secure_params.slice(:certificate, :key) }
let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) }
shared_examples_for 'put pages domain' do
it 'updates pages domain removing certificate' do
put api(route_secure_domain, user)
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil
end
it 'updates pages domain adding certificate' do
put api(route_domain, user), params_secure
pages_domain.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
it 'updates pages domain with expired certificate' do
put api(route_expired_domain, user), params_secure
pages_domain_expired.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
it 'updates pages domain with expired certificate not updating key' do
put api(route_secure_domain, user), params_secure_nokey
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
end
it 'fails to update pages domain adding certificate without key' do
put api(route_domain, user), params_secure_nokey
expect(response).to have_gitlab_http_status(400)
end
it 'fails to update pages domain adding certificate with missing chain' do
put api(route_domain, user), pages_domain_secure_missing_chain_params.slice(:certificate)
expect(response).to have_gitlab_http_status(400)
end
it 'fails to update pages domain with key missmatch' do
put api(route_secure_domain, user), pages_domain_secure_key_missmatch_params.slice(:certificate, :key)
expect(response).to have_gitlab_http_status(400)
end
end
context 'when domain is vacant' do
before do
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { put api(route_vacant_domain, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'put pages domain'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { put api(route_domain, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { put api(route_domain, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { put api(route_domain, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { put api(route_domain, user) }
end
end
end
describe 'DELETE /projects/:project_id/pages/domains/:domain' do
shared_examples_for 'delete pages domain' do
it 'deletes a pages domain' do
delete api(route_domain, user)
expect(response).to have_gitlab_http_status(204)
end
end
context 'when domain is vacant' do
before do
project.add_master(user)
end
it_behaves_like '404 response' do
let(:request) { delete api(route_vacant_domain, user) }
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it_behaves_like 'delete pages domain'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like '403 response' do
let(:request) { delete api(route_domain, user) }
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like '403 response' do
let(:request) { delete api(route_domain, user) }
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { delete api(route_domain, user) }
end
end
context 'when user is not a member' do
it_behaves_like '404 response' do
let(:request) { delete api(route_domain, user) }
end
end
end
end
...@@ -6,6 +6,8 @@ module JiraServiceHelper ...@@ -6,6 +6,8 @@ module JiraServiceHelper
properties = { properties = {
title: "JIRA tracker", title: "JIRA tracker",
url: JIRA_URL, url: JIRA_URL,
username: 'jira-user',
password: 'my-secret-password',
project_key: "JIRA", project_key: "JIRA",
jira_issue_transition_id: '1' jira_issue_transition_id: '1'
} }
......
...@@ -25,6 +25,14 @@ describe 'help/index' do ...@@ -25,6 +25,14 @@ describe 'help/index' do
end end
end end
describe 'instance configuration link' do
it 'is visible to guests' do
render
expect(rendered).to have_link(nil, help_instance_configuration_url)
end
end
def stub_user(user = double) def stub_user(user = double)
allow(view).to receive(:user_signed_in?).and_return(user) allow(view).to receive(:user_signed_in?).and_return(user)
end end
......
require 'rails_helper'
describe 'help/instance_configuration' do
describe 'General Sections:' do
let(:instance_configuration) { build(:instance_configuration)}
let(:settings) { instance_configuration.settings }
let(:ssh_settings) { settings[:ssh_algorithms_hashes] }
before do
assign(:instance_configuration, instance_configuration)
end
it 'has links to several sections' do
render
expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any?
expect(rendered).to have_link(nil, '#gitlab-pages')
expect(rendered).to have_link(nil, '#gitlab-ci')
end
it 'has several sections' do
render
expect(rendered).to have_css('h2#ssh-host-keys-fingerprints') if ssh_settings.any?
expect(rendered).to have_css('h2#gitlab-pages')
expect(rendered).to have_css('h2#gitlab-ci')
end
end
end
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