Commit 745d3538 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-jobs-list-components

parents 17c9e88c 5116d11a
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"airbnb-base",
"plugin:vue/recommended"
],
"globals": {
"__webpack_public_path__": true,
"gl": false,
"gon": false,
"localStorage": false
},
"parserOptions": {
"parser": "babel-eslint"
},
"plugins": [
"filenames",
"import",
"html",
"promise"
],
"settings": {
"html/html-extensions": [".html", ".html.raw"],
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
}
}
},
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+$"],
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__", "_links"] }],
"no-mixed-operators": 0,
"space-before-function-paren": 0,
"curly": 0,
"arrow-parens": 0,
"vue/html-self-closing": [
"error",
{
"html": {
"void": "always",
"normal": "never",
"component": "always"
},
"svg": "always",
"math": "always"
}
]
}
}
---
env:
browser: true
es6: true
extends:
- airbnb-base
- plugin:vue/recommended
globals:
__webpack_public_path__: true
gl: false
gon: false
localStorage: false
parserOptions:
parser: babel-eslint
plugins:
- filenames
- import
- html
- promise
settings:
html/html-extensions:
- ".html"
- ".html.raw"
import/resolver:
webpack:
config: "./config/webpack.config.js"
rules:
filenames/match-regex:
- error
- "^[a-z0-9_]+$"
import/no-commonjs: error
no-multiple-empty-lines:
- error
- max: 1
promise/catch-or-return: error
no-underscore-dangle:
- error
- allow:
- __
- _links
no-mixed-operators: off
vue/html-self-closing:
- error
- html:
void: always
normal: never
component: always
svg: always
math: always
## Conflicting rules with prettier:
space-before-function-paren: off
curly: off
arrow-parens: off
function-paren-newline: off
object-curly-newline: off
padded-blocks: off
# Disabled for now, to make the eslint 3 -> eslint 4 update smoother
## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite
indent: off
indent-legacy:
- error
- 2
- SwitchCase: 1
VariableDeclarator: 1
outerIIFEBody: 1
FunctionDeclaration:
parameters: 1
body: 1
FunctionExpression:
parameters: 1
body: 1
## Destructuring: https://eslint.org/docs/rules/prefer-destructuring
prefer-destructuring: off
## no-restricted-globals: https://eslint.org/docs/rules/no-restricted-globals
no-restricted-globals: off
## no-multi-assign: https://eslint.org/docs/rules/no-multi-assign
no-multi-assign: off
...@@ -591,7 +591,7 @@ ee_compat_check: ...@@ -591,7 +591,7 @@ ee_compat_check:
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?/ - /[\d-]+-stable(-ee)?/
- /^security-/ - /^security-/
- branches@gitlab-org/gitlab-ee - branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee - branches@gitlab/gitlab-ee
......
...@@ -2,6 +2,20 @@ ...@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.8.3 (2018-05-30)
### Fixed (4 changes)
- Replace Gitlab::REVISION with Gitlab.revision and handle installations without a .git directory. !19125
- Fix encoding of branch names on compare and new merge request page. !19143
- Fix remote mirror database inconsistencies when upgrading from EE to CE. !19196
- Fix local storage not being cleared after creating a new issue.
### Performance (1 change)
- Memoize Gitlab::Database.version.
## 10.8.2 (2018-05-28) ## 10.8.2 (2018-05-28)
### Security (3 changes) ### Security (3 changes)
......
...@@ -28,7 +28,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql ...@@ -28,7 +28,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.27' gem 'rugged', '~> 0.27'
gem 'grape-route-helpers', '~> 2.1.0' gem 'grape-path-helpers', '~> 1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
...@@ -219,7 +219,7 @@ gem 'asana', '~> 0.6.0' ...@@ -219,7 +219,7 @@ gem 'asana', '~> 0.6.0'
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration # Kubernetes integration
gem 'kubeclient', '~> 3.0' gem 'kubeclient', '~> 3.1.0'
# Sanitize user input # Sanitize user input
gem 'sanitize', '~> 2.0' gem 'sanitize', '~> 2.0'
...@@ -320,7 +320,7 @@ group :development, :test do ...@@ -320,7 +320,7 @@ group :development, :test do
gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4' gem 'pry-rails', '~> 0.3.4'
gem 'awesome_print', '~> 1.8.0', require: false gem 'awesome_print', require: false
gem 'fuubar', '~> 2.2.0' gem 'fuubar', '~> 2.2.0'
gem 'database_cleaner', '~> 1.5.0' gem 'database_cleaner', '~> 1.5.0'
......
...@@ -168,7 +168,7 @@ GEM ...@@ -168,7 +168,7 @@ GEM
diff-lcs (1.3) diff-lcs (1.3)
diffy (3.1.0) diffy (3.1.0)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2) doorkeeper (4.3.2)
railties (>= 4.2) railties (>= 4.2)
...@@ -348,7 +348,7 @@ GEM ...@@ -348,7 +348,7 @@ GEM
signet (~> 0.7) signet (~> 0.7)
gpgme (2.0.13) gpgme (2.0.13)
mini_portile2 (~> 2.1) mini_portile2 (~> 2.1)
grape (1.0.2) grape (1.0.3)
activesupport activesupport
builder builder
mustermann-grape (~> 1.0.0) mustermann-grape (~> 1.0.0)
...@@ -358,10 +358,10 @@ GEM ...@@ -358,10 +358,10 @@ GEM
grape-entity (0.7.1) grape-entity (0.7.1)
activesupport (>= 4.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-path-helpers (1.0.1)
activesupport activesupport (~> 4)
grape (>= 0.16.0) grape (~> 1.0)
rake rake (~> 12)
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.11.0) grpc (1.11.0)
...@@ -446,9 +446,9 @@ GEM ...@@ -446,9 +446,9 @@ GEM
knapsack (1.16.0) knapsack (1.16.0)
rake rake
timecop (>= 0.1.0) timecop (>= 0.1.0)
kubeclient (3.0.0) kubeclient (3.1.0)
http (~> 2.2.2) http (~> 2.2.2)
recursive-open-struct (~> 1.0.4) recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0) rest-client (~> 2.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
...@@ -698,7 +698,7 @@ GEM ...@@ -698,7 +698,7 @@ GEM
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (3.0.0)
json json
recursive-open-struct (1.0.5) recursive-open-struct (1.1.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.5) redis (3.3.5)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
...@@ -977,7 +977,7 @@ DEPENDENCIES ...@@ -977,7 +977,7 @@ DEPENDENCIES
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4) asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print (~> 1.8.0) awesome_print
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
batch-loader (~> 1.2.1) batch-loader (~> 1.2.1)
...@@ -1049,7 +1049,7 @@ DEPENDENCIES ...@@ -1049,7 +1049,7 @@ DEPENDENCIES
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-path-helpers (~> 1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
...@@ -1067,7 +1067,7 @@ DEPENDENCIES ...@@ -1067,7 +1067,7 @@ DEPENDENCIES
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.16) knapsack (~> 1.16)
kubeclient (~> 3.0) kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 3.1)
licensee (~> 8.9) licensee (~> 8.9)
......
...@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ ...@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
template: ` template: `
<section class="empty-state"> <section class="empty-state">
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-6 order-sm-last"> <div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside> <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div> </div>
<div class="col-xs-12 col-sm-6 order-sm-first"> <div class="col-12 col-md-6 order-md-first">
<div class="text-content"> <div class="text-content">
<h4>{{ contents.title }}</h4> <h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p> <p v-html="contents.content"></p>
......
/* global ListIssue */
import Vue from 'vue'; import Vue from 'vue';
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
...@@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({ ...@@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({
scrollHandler() { scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage); const currentPage = Math.floor(this.issues.length / this.perPage);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage if (
&& currentPage === this.page) { this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true; this.loadingNewPage = true;
this.page += 1; this.page += 1;
} }
......
<script> <script>
/* global ListIssue */ import $ from 'jquery';
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
import $ from 'jquery'; export default {
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect', name: 'BoardProjectSelect',
components: { components: {
loadingIcon, loadingIcon,
...@@ -48,7 +46,7 @@ ...@@ -48,7 +46,7 @@
selectable: true, selectable: true,
data: (term, callback) => { data: (term, callback) => {
this.loading = true; this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => { return Api.groupProjects(this.groupId, term, projects => {
this.loading = false; this.loading = false;
callback(projects); callback(projects);
}); });
...@@ -65,7 +63,7 @@ ...@@ -65,7 +63,7 @@
text: project => project.name, text: project => project.name,
}); });
}, },
}; };
</script> </script>
<template> <template>
......
/* global monaco */
import Disposable from './disposable'; import Disposable from './disposable';
import eventHub from '../../eventhub'; import eventHub from '../../eventhub';
......
...@@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
}); });
}; };
export const setFileMrChange = ({ state, commit }, { file, mrChange }) => { export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
}; };
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
const file = state.entries[path]; const file = state.entries[path];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
service service
...@@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn ...@@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
} }
}; };
export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode }); commit(types.SET_FILE_VIEWMODE, { file, viewMode });
}; };
......
...@@ -3,7 +3,7 @@ import service from '../../services'; ...@@ -3,7 +3,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, state, dispatch }, { commit, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
...@@ -32,7 +32,7 @@ export const getMergeRequestData = ( ...@@ -32,7 +32,7 @@ export const getMergeRequestData = (
}); });
export const getMergeRequestChanges = ( export const getMergeRequestChanges = (
{ commit, state, dispatch }, { commit, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
...@@ -58,7 +58,7 @@ export const getMergeRequestChanges = ( ...@@ -58,7 +58,7 @@ export const getMergeRequestChanges = (
}); });
export const getMergeRequestVersions = ( export const getMergeRequestVersions = (
{ commit, state, dispatch }, { commit, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
......
...@@ -3,10 +3,7 @@ import { __ } from '~/locale'; ...@@ -3,10 +3,7 @@ import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export const getProjectData = ( export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
...@@ -36,10 +33,7 @@ export const getProjectData = ( ...@@ -36,10 +33,7 @@ export const getProjectData = (
} }
}); });
export const getBranchData = ( export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if ( if (
typeof state.projects[`${projectId}`] === 'undefined' || typeof state.projects[`${projectId}`] === 'undefined' ||
...@@ -74,7 +68,7 @@ export const getBranchData = ( ...@@ -74,7 +68,7 @@ export const getBranchData = (
} }
}); });
export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) => export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service service
.getBranchData(projectId, branchId) .getBranchData(projectId, branchId)
.then(({ data }) => { .then(({ data }) => {
......
...@@ -5,7 +5,7 @@ import * as types from '../mutation_types'; ...@@ -5,7 +5,7 @@ import * as types from '../mutation_types';
import { findEntry } from '../utils'; import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker'; import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => { export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path); commit(types.TOGGLE_TREE_OPEN, path);
}; };
...@@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { ...@@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
} }
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service service
...@@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => export const getFiles = ({ state, commit }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) { if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId]; const selectedProject = state.projects[projectId];
......
...@@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { ...@@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
const currentProject = rootState.projects[rootState.currentProjectId]; const currentProject = rootState.projects[rootState.currentProjectId];
const commitStats = data.stats const commitStats = data.stats
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
additions: data.stats.additions, // eslint-disable-line indent additions: data.stats.additions, // eslint-disable-line indent-legacy
deletions: data.stats.deletions, // eslint-disable-line indent deletions: data.stats.deletions, // eslint-disable-line indent-legacy
}) // eslint-disable-line indent }) // eslint-disable-line indent-legacy
: ''; : '';
const commitMsg = sprintf( const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'), __('Your changes have been committed. Commit %{commitId} %{commitStats}'),
...@@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) => ...@@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) =>
), ),
); );
export const updateFilesAfterCommit = ( export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
{ commit, dispatch, state, rootState, rootGetters },
{ data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId]; const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = { const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`, commit_path: `${selectedProject.web_url}/commit/${data.id}`,
......
...@@ -8,7 +8,9 @@ import * as types from './mutation_types'; ...@@ -8,7 +8,9 @@ import * as types from './mutation_types';
let eTagPoll; let eTagPoll;
export const clearEtagPoll = () => (eTagPoll = null); export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop(); export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop();
export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart(); export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart();
......
...@@ -30,7 +30,7 @@ export default class IssuableForm { ...@@ -30,7 +30,7 @@ export default class IssuableForm {
} }
this.initAutosave(); this.initAutosave();
this.form.on('submit:success', this.handleSubmit); this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave); this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip(); this.initWip();
......
...@@ -84,7 +84,7 @@ export default class Job { ...@@ -84,7 +84,7 @@ export default class Job {
If the browser does not support position sticky, it returns the position as static. If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not If the browser does support sticky, then we allow the browser to handle it, if not
then we use a polyfill then we use a polyfill
**/ */
if (this.$topBar.css('position') !== 'static') return; if (this.$topBar.css('position') !== 'static') return;
StickyFill.add(this.$topBar); StickyFill.add(this.$topBar);
......
/* global Build */
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
...@@ -50,7 +48,8 @@ export default class JobMediator { ...@@ -50,7 +48,8 @@ export default class JobMediator {
} }
getJob() { getJob() {
return this.service.getJob() return this.service
.getJob()
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback());
} }
......
...@@ -9,7 +9,7 @@ delete window.translations; ...@@ -9,7 +9,7 @@ delete window.translations;
Translates `text` Translates `text`
@param text The text to be translated @param text The text to be translated
@returns {String} The translated text @returns {String} The translated text
**/ */
const gettext = locale.gettext.bind(locale); const gettext = locale.gettext.bind(locale);
/** /**
...@@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale); ...@@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale);
@param pluralText Plural text to translate (eg. '%d days') @param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2) @param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days') @returns {String} Translated text with the number replaced (eg. '2 days')
**/ */
const ngettext = (text, pluralText, count) => { const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
...@@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => { ...@@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => {
(eg. 'Context') (eg. 'Context')
@param key Is the dynamic variable you want to be translated @param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text @returns {String} Translated context based text
**/ */
const pgettext = (keyOrContext, key) => { const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const translated = gettext(normalizedKey).split('|'); const translated = gettext(normalizedKey).split('|');
......
...@@ -10,7 +10,7 @@ import _ from 'underscore'; ...@@ -10,7 +10,7 @@ import _ from 'underscore';
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
**/ */
export default (input, parameters, escapeParameters = true) => { export default (input, parameters, escapeParameters = true) => {
let output = input; let output = input;
......
...@@ -427,7 +427,7 @@ export default class MergeRequestTabs { ...@@ -427,7 +427,7 @@ export default class MergeRequestTabs {
If the browser does not support position sticky, it returns the position as static. If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not If the browser does support sticky, then we allow the browser to handle it, if not
then we default back to Bootstraps affix then we default back to Bootstraps affix
**/ */
if ($tabs.css('position') !== 'static') return; if ($tabs.css('position') !== 'static') return;
const $diffTabs = $('#diff-notes-app'); const $diffTabs = $('#diff-notes-app');
......
...@@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
commit(types.SET_NOTES_DATA, data); export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setNoteableData = ({ commit }, data) => export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
commit(types.SET_NOTEABLE_DATA, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setUserData = ({ commit }, data) => export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
commit(types.SET_USER_DATA, data); export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
export const setLastFetchedAt = ({ commit }, data) => export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) =>
commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) =>
commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) =>
commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) => export const fetchNotes = ({ commit }, path) =>
service service
...@@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) => ...@@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) =>
return res; return res;
}); });
export const removePlaceholderNotes = ({ commit }) => export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
commit(types.REMOVE_PLACEHOLDER_NOTES);
export const toggleResolveNote = ( export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) =>
{ commit },
{ endpoint, isResolved, discussion },
) =>
service service
.toggleResolveNote(endpoint, isResolved) .toggleResolveNote(endpoint, isResolved)
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
const mutationType = discussion const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
? types.UPDATE_DISCUSSION
: types.UPDATE_NOTE;
commit(mutationType, res); commit(mutationType, res);
}); });
...@@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => { ...@@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
export const toggleStateButtonLoading = ({ commit }, value) => export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value); commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => { export const emitStateChangedEvent = ({ getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { const event = new CustomEvent('issuable_vue_app:change', {
detail: { detail: {
data, data,
...@@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
loadAwardsHandler() loadAwardsHandler()
.then(awardsHandler => { .then(awardsHandler => {
awardsHandler.addAwardToEmojiBar( awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
votesBlock,
commandsChanges.emoji_award,
);
awardsHandler.scrollToAwards(); awardsHandler.scrollToAwards();
}) })
.catch(() => { .catch(() => {
...@@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}); });
} }
if ( if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
commandsChanges.spend_time != null ||
commandsChanges.time_estimate != null
) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
} }
} }
...@@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { ...@@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp.notes.forEach(note => { resp.notes.forEach(note => {
if (notesById[note.id]) { if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note); commit(types.UPDATE_NOTE, note);
} else if ( } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
note.type === constants.DISCUSSION_NOTE || const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
note.type === constants.DIFF_NOTE
) {
const discussion = utils.findNoteObjectById(
state.notes,
note.discussion_id,
);
if (discussion) { if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
...@@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => { ...@@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => {
method: 'poll', method: 'poll',
data: state, data: state,
successCallback: resp => successCallback: resp =>
resp resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)),
.json() errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
.then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () =>
Flash('Something went wrong while fetching latest comments.'),
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
...@@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => { ...@@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => {
.catch(() => Flash('Something went wrong while fetching latest comments.')); .catch(() => Flash('Something went wrong while fetching latest comments.'));
}; };
export const toggleAward = ( export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
{ commit, state, getters, dispatch },
{ awardName, noteId },
) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
}; };
export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { export const toggleAwardRequest = ({ dispatch }, data) => {
const { endpoint, awardName } = data; const { endpoint, awardName } = data;
return service return service
......
...@@ -5,7 +5,7 @@ import $ from 'jquery'; ...@@ -5,7 +5,7 @@ import $ from 'jquery';
* *
* Toggling this checkbox adds/removes a `remember_me` parameter to the * Toggling this checkbox adds/removes a `remember_me` parameter to the
* login buttons' href, which is passed on to the omniauth callback. * login buttons' href, which is passed on to the omniauth callback.
**/ */
export default class OAuthRememberMe { export default class OAuthRememberMe {
constructor(opts = {}) { constructor(opts = {}) {
......
...@@ -7,9 +7,10 @@ Vue.use(VueResource); ...@@ -7,9 +7,10 @@ Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => { export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING); commit(types.TOGGLE_MAIN_LOADING);
return Vue.http.get(state.endpoint) return Vue.http
.get(state.endpoint)
.then(res => res.json()) .then(res => res.json())
.then((response) => { .then(response => {
commit(types.TOGGLE_MAIN_LOADING); commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response); commit(types.SET_REPOS_LIST, response);
}); });
...@@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => { ...@@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => {
export const fetchList = ({ commit }, { repo, page }) => { export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } }) return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
.then((response) => {
const headers = response.headers; const headers = response.headers;
return response.json().then((resp) => { return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
}); });
}); });
}; };
// eslint-disable-next-line no-unused-vars
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
// eslint-disable-next-line no-unused-vars
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath); export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
......
...@@ -168,8 +168,8 @@ ...@@ -168,8 +168,8 @@
<a <a
:href="mr.mergeCommitPath" :href="mr.mergeCommitPath"
class="commit-sha js-mr-merged-commit-sha" class="commit-sha js-mr-merged-commit-sha"
v-text="mr.shortMergeCommitSha"
> >
{{ mr.shortMergeCommitSha }}
</a> </a>
<clipboard-button <clipboard-button
:title="__('Copy commit SHA to clipboard')" :title="__('Copy commit SHA to clipboard')"
......
...@@ -13,7 +13,7 @@ export default (Vue) => { ...@@ -13,7 +13,7 @@ export default (Vue) => {
@param text The text to be translated @param text The text to be translated
@returns {String} The translated text @returns {String} The translated text
**/ */
__, __,
/** /**
Translate the text with a number Translate the text with a number
...@@ -24,7 +24,7 @@ export default (Vue) => { ...@@ -24,7 +24,7 @@ export default (Vue) => {
@param pluralText Plural text to translate (eg. '%d days') @param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2) @param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days') @returns {String} Translated text with the number replaced (eg. '2 days')
**/ */
n__, n__,
/** /**
Translate context based text Translate context based text
...@@ -36,7 +36,7 @@ export default (Vue) => { ...@@ -36,7 +36,7 @@ export default (Vue) => {
(eg. 'Context') (eg. 'Context')
@param key Is the dynamic variable you want to be translated @param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text @returns {String} Translated context based text
**/ */
s__, s__,
sprintf, sprintf,
}, },
......
...@@ -74,12 +74,6 @@ body.modal-open { ...@@ -74,12 +74,6 @@ body.modal-open {
} }
} }
@include media-breakpoint-up(lg) {
.modal-full {
width: 98%;
}
}
.modal { .modal {
background-color: $black-transparent; background-color: $black-transparent;
z-index: 2100; z-index: 2100;
......
module Groups
class SharedProjectsController < Groups::ApplicationController
respond_to :json
before_action :group
skip_cross_project_access_check :index
def index
shared_projects = GroupProjectsFinder.new(
group: group,
current_user: current_user,
params: finder_params,
options: { only_shared: true }
).execute
serializer = GroupChildSerializer.new(current_user: current_user)
.with_pagination(request, response)
render json: serializer.represent(shared_projects)
end
private
def finder_params
@finder_params ||= begin
# Make the `search` param consistent for the frontend,
# which will be using `filter`.
params[:search] ||= params[:filter] if params[:filter]
params.permit(:sort, :search)
end
end
end
end
...@@ -957,6 +957,14 @@ class Repository ...@@ -957,6 +957,14 @@ class Repository
remote_branch: merge_request.target_branch) remote_branch: merge_request.target_branch)
end end
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
blob.load_all_data!
blob.data
end
def squash(user, merge_request) def squash(user, merge_request)
raw.squash(user, merge_request.id, branch: merge_request.target_branch, raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha, start_sha: merge_request.diff_start_sha,
...@@ -979,14 +987,6 @@ class Repository ...@@ -979,14 +987,6 @@ class Repository
::Commit.new(commit, @project) if commit ::Commit.new(commit, @project) if commit
end end
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
blob.load_all_data!
blob.data
end
def cache def cache
@cache ||= Gitlab::RepositoryCache.new(self) @cache ||= Gitlab::RepositoryCache.new(self)
end end
......
...@@ -206,10 +206,11 @@ class Service < ActiveRecord::Base ...@@ -206,10 +206,11 @@ class Service < ActiveRecord::Base
args.each do |arg| args.each do |arg|
class_eval %{ class_eval %{
def #{arg}? def #{arg}?
# '!!' is used because nil or empty string is converted to nil
if Gitlab.rails5? if Gitlab.rails5?
!ActiveModel::Type::Boolean::FALSE_VALUES.include?(#{arg}) !!ActiveRecord::Type::Boolean.new.cast(#{arg})
else else
ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) !!ActiveRecord::Type::Boolean.new.type_cast_from_database(#{arg})
end end
end end
} }
......
...@@ -31,7 +31,7 @@ class GroupChildEntity < Grape::Entity ...@@ -31,7 +31,7 @@ class GroupChildEntity < Grape::Entity
end end
# Project only attributes # Project only attributes
expose :star_count, expose :star_count, :archived,
if: lambda { |_instance, _options| project? } if: lambda { |_instance, _options| project? }
# Group only attributes # Group only attributes
......
...@@ -11,6 +11,7 @@ module ObjectStorage ...@@ -11,6 +11,7 @@ module ObjectStorage
ObjectStorageUnavailable = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError)
DIRECT_UPLOAD_TIMEOUT = 4.hours DIRECT_UPLOAD_TIMEOUT = 4.hours
DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes
TMP_UPLOAD_PATH = 'tmp/uploads'.freeze TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store module Store
...@@ -174,11 +175,12 @@ module ObjectStorage ...@@ -174,11 +175,12 @@ module ObjectStorage
id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
upload_path = File.join(TMP_UPLOAD_PATH, id) upload_path = File.join(TMP_UPLOAD_PATH, id)
connection = ::Fog::Storage.new(self.object_store_credentials) connection = ::Fog::Storage.new(self.object_store_credentials)
expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET
options = { 'Content-Type' => 'application/octet-stream' } options = { 'Content-Type' => 'application/octet-stream' }
{ {
ID: id, ID: id,
Timeout: DIRECT_UPLOAD_TIMEOUT,
GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at), GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at),
DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at), DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at),
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
......
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
#js-peek{ data: { env: Peek.env, #js-peek{ data: { env: Peek.env,
request_id: Peek.request_id, request_id: Peek.request_id,
peek_url: peek_routes.results_url, peek_url: "#{peek_routes_path}/results",
profile_url: url_for(safe_params.merge(lineprofiler: 'true')) }, profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env } class: Peek.env }
- active_tab = local_assigns.fetch(:active_tab, 'blank') - active_tab = local_assigns.fetch(:active_tab, 'blank')
- f = local_assigns.fetch(:f) - f = local_assigns.fetch(:f)
.project-import.row .project-import
.col-lg-12
.form-group.import-btn-container.clearfix .form-group.import-btn-container.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from Import project from
...@@ -44,7 +43,6 @@ ...@@ -44,7 +43,6 @@
- if git_import_enabled? - if git_import_enabled?
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
%hr %hr
= render "shared/import_form", f: f = render "shared/import_form", f: f
......
...@@ -35,11 +35,10 @@ ...@@ -35,11 +35,10 @@
%span (optional) %span (optional)
= f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
.form-group.visibility-level-setting = f.label :visibility_level, class: 'label-light' do
= f.label :visibility_level, class: 'label-light' do
Visibility Level Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
...@@ -12,8 +12,7 @@ ...@@ -12,8 +12,7 @@
.row .row
.col-sm-8.offset-sm-4.signin-with-google .col-sm-8.offset-sm-4.signin-with-google
- if @authorize_url - if @authorize_url
= link_to @authorize_url do = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
= image_tag('auth_buttons/signin_with_google.png', width: '191px')
= _('or') = _('or')
= link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- else - else
......
...@@ -13,9 +13,9 @@ ...@@ -13,9 +13,9 @@
= f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one? = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
.form-text.text-muted .form-text.text-muted
- if @teams.one? - if @teams.one?
This is the only available team. This is the only available team that you are a member of.
- else - else
The list shows all available teams. The list shows all available teams that you are a member of.
To create a team, To create a team,
= link_to "#{Gitlab.config.mattermost.host}/create_team" do = link_to "#{Gitlab.config.mattermost.host}/create_team" do
use Mattermost's interface use Mattermost's interface
......
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false) - ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
.form-group.row.import-url-data .form-group.import-url-data
= f.label :import_url, class: 'label-light' do = f.label :import_url, class: 'label-light' do
%span %span
= _('Git repository URL') = _('Git repository URL')
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
.card.prepend-top-20 .info-well.prepend-top-20
.well-segment
%ul %ul
%li %li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
......
- with_label = local_assigns.fetch(:with_label, true) - with_label = local_assigns.fetch(:with_label, true)
.form-group.row.visibility-level-setting .form-group.visibility-level-setting
- if with_label - if with_label
= f.label :visibility_level, class: 'col-form-label col-sm-2' do = f.label :visibility_level, class: 'col-form-label col-sm-2' do
Visibility Level Visibility Level
......
.row.empty-state.text-center .row.empty-state.text-center
.col-xs-12 .col-12
.svg-130.prepend-top-default .svg-130.prepend-top-default
= image_tag 'illustrations/issue-dashboard_results-without-filter.svg' = image_tag 'illustrations/issue-dashboard_results-without-filter.svg'
.col-xs-12 .col-12
.text-content .text-content
%h4 %h4
= _("Please select at least one filter to see results") = _("Please select at least one filter to see results")
.row.empty-state .row.empty-state
.col-xs-12 .col-12
.svg-content .svg-content
= image_tag image_path = image_tag image_path
.col-xs-12 .col-12
.text-content.text-center .text-content.text-center
= yield = yield
%p.form-text.text-muted
Add
= link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
to help your contributors communicate effectively!
...@@ -30,7 +30,4 @@ ...@@ -30,7 +30,4 @@
merge request from being merged before it's ready. merge request from being merged before it's ready.
- if no_issuable_templates && can?(current_user, :push_code, issuable.project) - if no_issuable_templates && can?(current_user, :push_code, issuable.project)
%p.form-text.text-muted = render 'shared/issuable/form/default_templates'
Add
= link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
to help your contributors communicate effectively!
---
title: Replace Gitlab::REVISION with Gitlab.revision and handle installations without
a .git directory
merge_request: 19125
author:
type: fixed
---
title: Fix UI broken in line profiling modal due to Bootstrap 4
merge_request: 19253
author: Takuya Noguchi
type: other
---
title: Updates the version of kubeclient from 3.0 to 3.1.0
merge_request: 19199
author:
type: other
---
title: Only preload member records for the relevant projects/groups/user in projects
API
merge_request:
author:
type: performance
---
title: Fix remote mirror database inconsistencies when upgrading from EE to CE
merge_request: 19196
author:
type: fixed
---
title: Import bitbucket issues that are reported by an anonymous user
merge_request: 18199
author: bartl
type: fixed
---
title: Missing timeout value in object storage pre-authorization
merge_request: 19201
author:
type: fixed
---
title: Fix &nbsp; after sign-in with Google button
merge_request:
author:
type: fixed
---
title: Updated Mattermost integration to use API v4 and only allow creation of Mattermost slash commands in the current user's teams
merge_request: 19043
author: Harrison Healey
type: changed
--- ---
title: Memoize Gitlab::Database.version title: Replace grape-route-helpers with our own grape-path-helpers
merge_request: merge_request:
author: author:
type: performance type: performance
---
title: Fix encoding of branch names on compare and new merge request page
merge_request: 19143
author:
type: fixed
if defined?(GrapeRouteHelpers)
module GrapeRouteHelpers
module AllRoutes
# Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment.
#
# Without the following fix, when two helper methods are the same, but have different arguments
# (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2))
# if the helper method with the least number of arguments is defined first (because the route was defined first)
# then it will shadow the longer route.
#
# The fix is to sort descending by amount of arguments
def decorated_routes
@decorated_routes ||= all_routes
.map { |r| DecoratedRoute.new(r) }
.sort_by { |r| -r.dynamic_path_segments.count }
end
end
class DecoratedRoute
# GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition.
#
# Without the following fix, we get this on route helpers generation:
#
# => undefined method `scan' for ["v3", "v4"]
#
# 2.0.0 implementation of this method:
#
# ```
# def route_versions
# version_pattern = /[^\[",\]\s]+/
# if route_version
# route_version.scan(version_pattern)
# else
# [nil]
# end
# end
# ```
def route_versions
return [nil] if route_version.nil? || route_version.empty?
if route_version.is_a?(String)
version_pattern = /[^\[",\]\s]+/
route_version.scan(version_pattern)
else
route_version
end
end
end
end
end
...@@ -30,6 +30,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -30,6 +30,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :variables, only: [:show, :update] resource :variables, only: [:show, :update]
resources :children, only: [:index] resources :children, only: [:index]
resources :shared_projects, only: [:index]
resources :labels, except: [:show] do resources :labels, except: [:show] do
post :toggle_subscription, on: :member post :toggle_subscription, on: :member
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
> >
**Note:** Custom Git hooks must be configured on the filesystem of the GitLab **Note:** Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks. server. Only GitLab server administrators will be able to complete these tasks.
Please explore [webhooks] as an option if you do not Please explore [webhooks] and [CI] as an option if you do not
have filesystem access. For a user configurable Git hook interface, see have filesystem access. For a user configurable Git hook interface, see
[Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html), [Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html),
available in GitLab Enterprise Edition. available in GitLab Enterprise Edition.
...@@ -80,6 +80,7 @@ STDERR takes precedence over STDOUT. ...@@ -80,6 +80,7 @@ STDERR takes precedence over STDOUT.
![Custom message from custom Git hook](img/custom_hooks_error_msg.png) ![Custom message from custom Git hook](img/custom_hooks_error_msg.png)
[CI]: ../ci/README.md
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks [hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
[webhooks]: ../user/project/integrations/webhooks.md [webhooks]: ../user/project/integrations/webhooks.md
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 [5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
......
...@@ -25,7 +25,9 @@ options: ...@@ -25,7 +25,9 @@ options:
errors when the Omnibus package tries to alter permissions. Note that GitLab errors when the Omnibus package tries to alter permissions. Note that GitLab
and other bundled components do **not** run as `root` but as non-privileged and other bundled components do **not** run as `root` but as non-privileged
users. The recommendation for `no_root_squash` is to allow the Omnibus package users. The recommendation for `no_root_squash` is to allow the Omnibus package
to set ownership and permissions on files, as needed. to set ownership and permissions on files, as needed. In some cases where the
`no_root_squash` option is not available, the `root` flag can achieve the same
result.
- `sync` - Force synchronous behavior. Default is asynchronous and under certain - `sync` - Force synchronous behavior. Default is asynchronous and under certain
circumstances it could lead to data loss if a failure occurs before data has circumstances it could lead to data loss if a failure occurs before data has
synced. synced.
......
...@@ -24,7 +24,6 @@ gitlab-rake gitlab:storage:migrate_to_hashed ...@@ -24,7 +24,6 @@ gitlab-rake gitlab:storage:migrate_to_hashed
```bash ```bash
rake gitlab:storage:migrate_to_hashed rake gitlab:storage:migrate_to_hashed
``` ```
You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen. You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
...@@ -52,7 +51,6 @@ gitlab-rake gitlab:storage:legacy_projects ...@@ -52,7 +51,6 @@ gitlab-rake gitlab:storage:legacy_projects
```bash ```bash
rake gitlab:storage:legacy_projects rake gitlab:storage:legacy_projects
``` ```
------ ------
...@@ -86,7 +84,6 @@ gitlab-rake gitlab:storage:hashed_projects ...@@ -86,7 +84,6 @@ gitlab-rake gitlab:storage:hashed_projects
```bash ```bash
rake gitlab:storage:hashed_projects rake gitlab:storage:hashed_projects
``` ```
------ ------
...@@ -120,7 +117,6 @@ gitlab-rake gitlab:storage:legacy_attachments ...@@ -120,7 +117,6 @@ gitlab-rake gitlab:storage:legacy_attachments
```bash ```bash
rake gitlab:storage:legacy_attachments rake gitlab:storage:legacy_attachments
``` ```
------ ------
...@@ -137,7 +133,6 @@ gitlab-rake gitlab:storage:list_legacy_attachments ...@@ -137,7 +133,6 @@ gitlab-rake gitlab:storage:list_legacy_attachments
```bash ```bash
rake gitlab:storage:list_legacy_attachments rake gitlab:storage:list_legacy_attachments
``` ```
## List attachments on Hashed storage ## List attachments on Hashed storage
...@@ -154,7 +149,6 @@ gitlab-rake gitlab:storage:hashed_attachments ...@@ -154,7 +149,6 @@ gitlab-rake gitlab:storage:hashed_attachments
```bash ```bash
rake gitlab:storage:hashed_attachments rake gitlab:storage:hashed_attachments
``` ```
------ ------
...@@ -171,7 +165,6 @@ gitlab-rake gitlab:storage:list_hashed_attachments ...@@ -171,7 +165,6 @@ gitlab-rake gitlab:storage:list_hashed_attachments
```bash ```bash
rake gitlab:storage:list_hashed_attachments rake gitlab:storage:list_hashed_attachments
``` ```
[storage-types]: ../repository_storage_types.md [storage-types]: ../repository_storage_types.md
......
...@@ -22,7 +22,7 @@ There are a few rules to get your merge request accepted: ...@@ -22,7 +22,7 @@ There are a few rules to get your merge request accepted:
1. If your merge request includes UX, frontend and backend changes [^1], it must 1. If your merge request includes UX, frontend and backend changes [^1], it must
be **approved by a [UX team member, a frontend and a backend maintainer][team]**. be **approved by a [UX team member, a frontend and a backend maintainer][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must 1. If your merge request includes a new dependency or a filesystem change, it must
be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details. be *approved by a [Distribution team member][team]*. See how to work with the [Distribution team for more details.](https://about.gitlab.com/handbook/engineering/dev-backend/distribution/)
1. To lower the amount of merge requests maintainers need to review, you can 1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][projects] for a first review. ask or assign any [reviewers][projects] for a first review.
1. If you need some guidance (e.g. it's your first merge request), feel free 1. If you need some guidance (e.g. it's your first merge request), feel free
......
...@@ -368,27 +368,17 @@ resolve when you add the indentation to the equation. ...@@ -368,27 +368,17 @@ resolve when you add the indentation to the equation.
EE-specific views should be placed in `ee/app/views/`, using extra EE-specific views should be placed in `ee/app/views/`, using extra
sub-directories if appropriate. sub-directories if appropriate.
#### Using `render_if_exists`
Instead of using regular `render`, we should use `render_if_exists`, which Instead of using regular `render`, we should use `render_if_exists`, which
will not render anything if it cannot find the specific partial. We use this will not render anything if it cannot find the specific partial. We use this
so that we could put `render_if_exists` in CE, keeping code the same between so that we could put `render_if_exists` in CE, keeping code the same between
CE and EE. CE and EE.
Also, it should search for the EE partial first, and then CE partial, and
then if nothing found, render nothing.
This has two uses:
- CE renders nothing, and EE renders its EE partial.
- CE renders its CE partial, and EE renders its EE partial, while the view
file stays the same.
The advantages of this: The advantages of this:
- Minimal code difference between CE and EE. - Minimal code difference between CE and EE.
- Very clear hints about where we're extending EE views while reading CE codes. - Very clear hints about where we're extending EE views while reading CE codes.
- Whenever we want to show something different in CE, we could just add CE
partials. Same applies the other way around. If we just use
`render_if_exists`, it would be very easy to change the content in EE.
The disadvantage of this: The disadvantage of this:
...@@ -396,6 +386,42 @@ The disadvantage of this: ...@@ -396,6 +386,42 @@ The disadvantage of this:
port `render_if_exists` to CE. port `render_if_exists` to CE.
- If we have typos in the partial name, it would be silently ignored. - If we have typos in the partial name, it would be silently ignored.
#### Using `render_ce`
For `render` and `render_if_exists`, they search for the EE partial first,
and then CE partial. They would only render a particular partial, not all
partials with the same name. We could take the advantage of this, so that
the same partial path (e.g. `shared/issuable/form/default_templates`) could
be referring to the CE partial in CE (i.e.
`app/views/shared/issuable/form/_default_templates.html.haml`), while EE
partial in EE (i.e.
`ee/app/views/shared/issuable/form/_default_templates.html.haml`). This way,
we could show different things between CE and EE.
However sometimes we would also want to reuse the CE partial in EE partial
because we might just want to add something to the existing CE partial. We
could workaround this by adding another partial with a different name, but it
would be tedious to do so.
In this case, we could as well just use `render_ce` which would ignore any EE
partials. One example would be
`ee/app/views/shared/issuable/form/_default_templates.html.haml`:
``` haml
- if @project.feature_available?(:issuable_default_templates)
= render_ce 'shared/issuable/form/default_templates'
- elsif show_promotions?
= render 'shared/promotions/promote_issue_templates'
```
In the above example, we can't use
`render 'shared/issuable/form/default_templates'` because it would find the
same EE partial, causing infinite recursion. Instead, we could use `render_ce`
so it ignores any partials in `ee/` and then it would render the CE partial
(i.e. `app/views/shared/issuable/form/_default_templates.html.haml`)
for the same path (i.e. `shared/issuable/form/default_templates`). This way
we could easily wrap around the CE partial.
### Code in `lib/` ### Code in `lib/`
Place EE-specific logic in the top-level `EE` module namespace. Namespace the Place EE-specific logic in the top-level `EE` module namespace. Namespace the
......
...@@ -72,6 +72,8 @@ website with GitLab Pages ...@@ -72,6 +72,8 @@ website with GitLab Pages
**Other features:** **Other features:**
- [Wiki](wiki/index.md): Document your GitLab project in an integrated Wiki
- [Snippets](../snippets.md): Store, share and collaborate on code snippets
- [Cycle Analytics](cycle_analytics.md): Review your development lifecycle - [Cycle Analytics](cycle_analytics.md): Review your development lifecycle
- [Syntax highlighting](highlighting.md): An alternative to customize - [Syntax highlighting](highlighting.md): An alternative to customize
your code blocks, overriding GitLab's default choice of language your code blocks, overriding GitLab's default choice of language
......
# Snippets # Snippets
Snippets are little bits of code or text. With GitLab Snippets you can store and share bits of code and text with other users.
![GitLab Snippet](img/gitlab_snippet.png) ![GitLab Snippet](img/gitlab_snippet.png)
There are 2 types of snippets - project snippets and personal snippets. There are 2 types of snippets, personal snippets and project snippets.
## Comments ## Personal snippets
With GitLab Snippets you engage in a conversation about that piece of code,
facilitating the collaboration among users.
> **Note:** Personal snippets are not related to any project and can be created completely
Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in [GitLab Community Edition 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#comments-for-personal-snippets). independently. There are 3 visibility levels that can be set, public, internal
and private. See [Public access](../public_access/public_access.md) for more information.
## Project snippets ## Project snippets
Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information. Project snippets are always related to a specific project.
See [Project's features](project/index.md#project-39-s-features) for more information.
## Personal snippets ## Discover snippets
There are two main ways of how you can discover snippets in GitLab.
Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information). For exploring all snippets that are visible to you, you can go to the Snippets
dashboard of your GitLab instance via the top navigation. For GitLab.com you can
find it [here](https://gitlab.com/dashboard/snippets). This navigates you to an
overview that shows snippets you created and allows you to explore all snippets.
If you want to discover snippets that belong to a specific project, you can navigate
to the Snippets page via the left side navigation on the project page.
## Snippet comments
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in GitLab 9.2.
With GitLab Snippets you engage in a conversation about that piece of code,
facilitating the collaboration among users.
## Downloading snippets ## Downloading snippets
You can download the raw content of a snippet. You can download the raw content of a snippet.
By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). By default snippets will be downloaded with Linux-style line endings (`LF`). If
you want to preserve the original line endings you need to add a parameter `line_ending=raw`
(e.g., `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
## Embedded Snippets ## Embedded snippets
> Introduced in GitLab 10.8. > Introduced in GitLab 10.8.
......
...@@ -832,8 +832,8 @@ module API ...@@ -832,8 +832,8 @@ module API
class ProjectWithAccess < Project class ProjectWithAccess < Project
expose :permissions do expose :permissions do
expose :project_access, using: Entities::ProjectAccess do |project, options| expose :project_access, using: Entities::ProjectAccess do |project, options|
if options.key?(:project_members) if options[:project_members]
(options[:project_members] || []).find { |member| member.source_id == project.id } options[:project_members].find { |member| member.source_id == project.id }
else else
project.project_member(options[:current_user]) project.project_member(options[:current_user])
end end
...@@ -841,8 +841,8 @@ module API ...@@ -841,8 +841,8 @@ module API
expose :group_access, using: Entities::GroupAccess do |project, options| expose :group_access, using: Entities::GroupAccess do |project, options|
if project.group if project.group
if options.key?(:group_members) if options[:group_members]
(options[:group_members] || []).find { |member| member.source_id == project.namespace_id } options[:group_members].find { |member| member.source_id == project.namespace_id }
else else
project.group.group_member(options[:current_user]) project.group.group_member(options[:current_user])
end end
...@@ -853,13 +853,24 @@ module API ...@@ -853,13 +853,24 @@ module API
def self.preload_relation(projects_relation, options = {}) def self.preload_relation(projects_relation, options = {})
relation = super(projects_relation, options) relation = super(projects_relation, options)
unless options.key?(:group_members) # MySQL doesn't support LIMIT inside an IN subquery
relation = relation.preload(group: [group_members: [:source, user: [notification_settings: :source]]]) if Gitlab::Database.mysql?
project_ids = relation.pluck('projects.id')
namespace_ids = relation.pluck(:namespace_id)
else
project_ids = relation.select('projects.id')
namespace_ids = relation.select(:namespace_id)
end end
unless options.key?(:project_members) options[:project_members] = options[:current_user]
relation = relation.preload(project_members: [:source, user: [notification_settings: :source]]) .project_members
end .where(source_id: project_ids)
.preload(:source, user: [notification_settings: :source])
options[:group_members] = options[:current_user]
.group_members
.where(source_id: namespace_ids)
.preload(:source, user: [notification_settings: :source])
relation relation
end end
......
module API module API
module Helpers module Helpers
module RelatedResourcesHelpers module RelatedResourcesHelpers
include GrapeRouteHelpers::NamedRouteMatcher include GrapePathHelpers::NamedRouteMatcher
def issues_available?(project, options) def issues_available?(project, options)
available?(:issues, project, options[:current_user]) available?(:issues, project, options[:current_user])
......
...@@ -58,16 +58,9 @@ module API ...@@ -58,16 +58,9 @@ module API
projects = paginate(projects) projects = paginate(projects)
projects, options = with_custom_attributes(projects, options) projects, options = with_custom_attributes(projects, options)
if current_user
project_members = current_user.project_members.preload(:source, user: [notification_settings: :source])
group_members = current_user.group_members.preload(:source, user: [notification_settings: :source])
end
options = options.reverse_merge( options = options.reverse_merge(
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
statistics: params[:statistics], statistics: params[:statistics],
project_members: project_members,
group_members: group_members,
current_user: current_user current_user: current_user
) )
options[:with] = Entities::BasicProjectDetails if params[:simple] options[:with] = Entities::BasicProjectDetails if params[:simple]
......
...@@ -129,6 +129,7 @@ module API ...@@ -129,6 +129,7 @@ module API
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction", optional :"#{type}_key_restriction",
......
...@@ -12,7 +12,7 @@ module Bitbucket ...@@ -12,7 +12,7 @@ module Bitbucket
end end
def author def author
raw.fetch('reporter', {}).fetch('username', nil) raw.dig('reporter', 'username')
end end
def description def description
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
gon.asset_host = ActionController::Base.asset_host gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts') gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
gon.gitlab_url = Gitlab.config.gitlab.url gon.gitlab_url = Gitlab.config.gitlab.url
......
module Gitlab
module HashedStorage
module RakeHelper
def self.batch_size
ENV.fetch('BATCH', 200).to_i
end
def self.listing_limit
ENV.fetch('LIMIT', 500).to_i
end
def self.project_id_batches(&block)
Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
ids = relation.pluck(:id)
yield ids.min, ids.max
end
end
def self.legacy_attachments_relation
Upload.joins(<<~SQL).where('projects.storage_version < :version OR projects.storage_version IS NULL', version: Project::HASHED_STORAGE_FEATURES[:attachments])
JOIN projects
ON (uploads.model_type='Project' AND uploads.model_id=projects.id)
SQL
end
def self.hashed_attachments_relation
Upload.joins(<<~SQL).where('projects.storage_version >= :version', version: Project::HASHED_STORAGE_FEATURES[:attachments])
JOIN projects
ON (uploads.model_type='Project' AND uploads.model_id=projects.id)
SQL
end
def self.relation_summary(relation_name, relation)
relation_count = relation.count
$stdout.puts "* Found #{relation_count} #{relation_name}".color(:green)
relation_count
end
def self.projects_list(relation_name, relation)
listing(relation_name, relation.with_route) do |project|
$stdout.puts " - #{project.full_path} (id: #{project.id})".color(:red)
end
end
def self.attachments_list(relation_name, relation)
listing(relation_name, relation) do |upload|
$stdout.puts " - #{upload.path} (id: #{upload.id})".color(:red)
end
end
def self.listing(relation_name, relation)
relation_count = relation_summary(relation_name, relation)
return unless relation_count > 0
limit = listing_limit
if relation_count > limit
$stdout.puts " ! Displaying first #{limit} #{relation_name}..."
end
relation.find_each(batch_size: batch_size).with_index do |element, index|
yield element
break if index + 1 >= limit
end
end
end
end
end
...@@ -9,6 +9,7 @@ module Gitlab ...@@ -9,6 +9,7 @@ module Gitlab
end end
def author_line(author) def author_line(author)
author ||= "Anonymous"
"*Created by: #{author}*\n\n" "*Created by: #{author}*\n\n"
end end
end end
......
...@@ -15,6 +15,11 @@ module Gitlab ...@@ -15,6 +15,11 @@ module Gitlab
def perform_request(env) def perform_request(env)
if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
if relative_url_root = Rails.application.config.relative_url_root
env['SCRIPT_NAME'] = ""
env['REQUEST_PATH'].sub!(/\A#{Regexp.escape(relative_url_root)}/, '')
end
super(env) super(env)
else else
@app.call(env) @app.call(env)
......
module Mattermost module Mattermost
class Command < Client class Command < Client
def create(params) def create(params)
response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", response = session_post('/api/v4/commands',
body: params.to_json) body: params.to_json)
response['token'] response['token']
......
...@@ -112,7 +112,7 @@ module Mattermost ...@@ -112,7 +112,7 @@ module Mattermost
end end
def destroy def destroy
post('/api/v3/users/logout') post('/api/v4/users/logout')
end end
def oauth_uri def oauth_uri
...@@ -120,7 +120,7 @@ module Mattermost ...@@ -120,7 +120,7 @@ module Mattermost
@oauth_uri = nil @oauth_uri = nil
response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) response = get('/oauth/gitlab/login', follow_redirects: false, format: 'text/html')
return unless (300...400) === response.code return unless (300...400) === response.code
redirect_uri = response.headers['location'] redirect_uri = response.headers['location']
......
module Mattermost module Mattermost
class Team < Client class Team < Client
# Returns **all** teams for an admin # Returns all teams that the current user is a member of
def all def all
session_get('/api/v3/teams/all').values session_get("/api/v4/users/me/teams")
end end
# Creates a team on the linked Mattermost instance, the team admin will be the # Creates a team on the linked Mattermost instance, the team admin will be the
# `current_user` passed to the Mattermost::Client instance # `current_user` passed to the Mattermost::Client instance
def create(name:, display_name:, type:) def create(name:, display_name:, type:)
session_post('/api/v3/teams/create', body: { session_post('/api/v4/teams', body: {
name: name, name: name,
display_name: display_name, display_name: display_name,
type: type type: type
......
...@@ -41,10 +41,10 @@ module Peek ...@@ -41,10 +41,10 @@ module Peek
] ]
end.sort_by{ |a,b,c,d,e,f| -f } end.sort_by{ |a,b,c,d,e,f| -f }
output = "<div class='modal-dialog modal-full'><div class='modal-content'>" output = "<div class='modal-dialog modal-lg'><div class='modal-content'>"
output << "<div class='modal-header'>" output << "<div class='modal-header'>"
output << "<button class='close btn btn-link btn-sm' type='button' data-dismiss='modal'>X</button>"
output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>" output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>"
output << "<button class='close' type='button' data-dismiss='modal' aria-label='close'><span aria-hidden='true'>&times;</span></button>"
output << "</div>" output << "</div>"
output << "<div class='modal-body'>" output << "<div class='modal-body'>"
......
...@@ -3,6 +3,7 @@ namespace :gitlab do ...@@ -3,6 +3,7 @@ namespace :gitlab do
desc 'GitLab | Storage | Migrate existing projects to Hashed Storage' desc 'GitLab | Storage | Migrate existing projects to Hashed Storage'
task migrate_to_hashed: :environment do task migrate_to_hashed: :environment do
legacy_projects_count = Project.with_unmigrated_storage.count legacy_projects_count = Project.with_unmigrated_storage.count
helper = Gitlab::HashedStorage::RakeHelper
if legacy_projects_count == 0 if legacy_projects_count == 0
puts 'There are no projects requiring storage migration. Nothing to do!' puts 'There are no projects requiring storage migration. Nothing to do!'
...@@ -10,9 +11,9 @@ namespace :gitlab do ...@@ -10,9 +11,9 @@ namespace :gitlab do
next next
end end
print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}" print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}"
project_id_batches do |start, finish| helper.project_id_batches do |start, finish|
StorageMigratorWorker.perform_async(start, finish) StorageMigratorWorker.perform_async(start, finish)
print '.' print '.'
...@@ -23,118 +24,50 @@ namespace :gitlab do ...@@ -23,118 +24,50 @@ namespace :gitlab do
desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage' desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage'
task legacy_projects: :environment do task legacy_projects: :environment do
relation_summary('projects', Project.without_storage_feature(:repository)) helper = Gitlab::HashedStorage::RakeHelper
helper.relation_summary('projects using Legacy Storage', Project.without_storage_feature(:repository))
end end
desc 'Gitlab | Storage | List existing projects using Legacy Storage' desc 'Gitlab | Storage | List existing projects using Legacy Storage'
task list_legacy_projects: :environment do task list_legacy_projects: :environment do
projects_list('projects using Legacy Storage', Project.without_storage_feature(:repository)) helper = Gitlab::HashedStorage::RakeHelper
helper.projects_list('projects using Legacy Storage', Project.without_storage_feature(:repository))
end end
desc 'Gitlab | Storage | Summary of existing projects using Hashed Storage' desc 'Gitlab | Storage | Summary of existing projects using Hashed Storage'
task hashed_projects: :environment do task hashed_projects: :environment do
relation_summary('projects using Hashed Storage', Project.with_storage_feature(:repository)) helper = Gitlab::HashedStorage::RakeHelper
helper.relation_summary('projects using Hashed Storage', Project.with_storage_feature(:repository))
end end
desc 'Gitlab | Storage | List existing projects using Hashed Storage' desc 'Gitlab | Storage | List existing projects using Hashed Storage'
task list_hashed_projects: :environment do task list_hashed_projects: :environment do
projects_list('projects using Hashed Storage', Project.with_storage_feature(:repository)) helper = Gitlab::HashedStorage::RakeHelper
helper.projects_list('projects using Hashed Storage', Project.with_storage_feature(:repository))
end end
desc 'Gitlab | Storage | Summary of project attachments using Legacy Storage' desc 'Gitlab | Storage | Summary of project attachments using Legacy Storage'
task legacy_attachments: :environment do task legacy_attachments: :environment do
relation_summary('attachments using Legacy Storage', legacy_attachments_relation) helper = Gitlab::HashedStorage::RakeHelper
helper.relation_summary('attachments using Legacy Storage', helper.legacy_attachments_relation)
end end
desc 'Gitlab | Storage | List existing project attachments using Legacy Storage' desc 'Gitlab | Storage | List existing project attachments using Legacy Storage'
task list_legacy_attachments: :environment do task list_legacy_attachments: :environment do
attachments_list('attachments using Legacy Storage', legacy_attachments_relation) helper = Gitlab::HashedStorage::RakeHelper
helper.attachments_list('attachments using Legacy Storage', helper.legacy_attachments_relation)
end end
desc 'Gitlab | Storage | Summary of project attachments using Hashed Storage' desc 'Gitlab | Storage | Summary of project attachments using Hashed Storage'
task hashed_attachments: :environment do task hashed_attachments: :environment do
relation_summary('attachments using Hashed Storage', hashed_attachments_relation) helper = Gitlab::HashedStorage::RakeHelper
helper.relation_summary('attachments using Hashed Storage', helper.hashed_attachments_relation)
end end
desc 'Gitlab | Storage | List existing project attachments using Hashed Storage' desc 'Gitlab | Storage | List existing project attachments using Hashed Storage'
task list_hashed_attachments: :environment do task list_hashed_attachments: :environment do
attachments_list('attachments using Hashed Storage', hashed_attachments_relation) helper = Gitlab::HashedStorage::RakeHelper
end helper.attachments_list('attachments using Hashed Storage', helper.hashed_attachments_relation)
def batch_size
ENV.fetch('BATCH', 200).to_i
end
def project_id_batches(&block)
Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
ids = relation.pluck(:id)
yield ids.min, ids.max
end
end
def legacy_attachments_relation
Upload.joins(<<~SQL).where('projects.storage_version < :version OR projects.storage_version IS NULL', version: Project::HASHED_STORAGE_FEATURES[:attachments])
JOIN projects
ON (uploads.model_type='Project' AND uploads.model_id=projects.id)
SQL
end
def hashed_attachments_relation
Upload.joins(<<~SQL).where('projects.storage_version >= :version', version: Project::HASHED_STORAGE_FEATURES[:attachments])
JOIN projects
ON (uploads.model_type='Project' AND uploads.model_id=projects.id)
SQL
end
def relation_summary(relation_name, relation)
relation_count = relation.count
puts "* Found #{relation_count} #{relation_name}".color(:green)
relation_count
end
def projects_list(relation_name, relation)
relation_count = relation_summary(relation_name, relation)
projects = relation.with_route
limit = ENV.fetch('LIMIT', 500).to_i
return unless relation_count > 0
puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit
counter = 0
projects.find_in_batches(batch_size: batch_size) do |batch|
batch.each do |project|
counter += 1
puts " - #{project.full_path} (id: #{project.id})".color(:red)
return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
def attachments_list(relation_name, relation)
relation_count = relation_summary(relation_name, relation)
limit = ENV.fetch('LIMIT', 500).to_i
return unless relation_count > 0
puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit
counter = 0
relation.find_in_batches(batch_size: batch_size) do |batch|
batch.each do |upload|
counter += 1
puts " - #{upload.path} (id: #{upload.id})".color(:red)
return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end end
end end
end end
require 'spec_helper'
describe Groups::SharedProjectsController do
def get_shared_projects(params = {})
get :index, params.reverse_merge(format: :json, group_id: group.full_path)
end
def share_project(project)
Projects::GroupLinks::CreateService.new(
project,
user,
link_group_access: ProjectGroupLink::DEVELOPER
).execute(group)
end
set(:group) { create(:group) }
set(:user) { create(:user) }
set(:shared_project) do
shared_project = create(:project, namespace: user.namespace)
share_project(shared_project)
shared_project
end
let(:json_project_ids) { json_response.map { |project_info| project_info['id'] } }
before do
sign_in(user)
end
describe 'GET #index' do
it 'returns only projects shared with the group' do
create(:project, namespace: group)
get_shared_projects
expect(json_project_ids).to contain_exactly(shared_project.id)
end
it 'allows filtering shared projects' do
project = create(:project, :archived, namespace: user.namespace, name: "Searching for")
share_project(project)
get_shared_projects(filter: 'search')
expect(json_project_ids).to contain_exactly(project.id)
end
it 'allows sorting projects' do
shared_project.update!(name: 'bbb')
second_project = create(:project, namespace: user.namespace, name: 'aaaa')
share_project(second_project)
get_shared_projects(sort: 'name_asc')
expect(json_project_ids).to eq([second_project.id, shared_project.id])
end
end
end
...@@ -591,6 +591,20 @@ describe 'Issues' do ...@@ -591,6 +591,20 @@ describe 'Issues' do
end end
end end
it 'clears local storage after creating a new issue', :js do
2.times do
visit new_project_issue_path(project)
wait_for_requests
expect(page).to have_field('Title', with: '')
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
click_button 'Submit issue'
end
end
context 'dropzone upload file', :js do context 'dropzone upload file', :js do
before do before do
visit new_project_issue_path(project) visit new_project_issue_path(project)
......
...@@ -64,7 +64,7 @@ feature 'Setup Mattermost slash commands', :js do ...@@ -64,7 +64,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost' click_link 'Add to Mattermost'
expect(page).to have_content('The team where the slash commands will be used in') expect(page).to have_content('The team where the slash commands will be used in')
expect(page).to have_content('This is the only available team.') expect(page).to have_content('This is the only available team that you are a member of.')
end end
it 'shows a disabled prefilled select if user is a member of 1 team' do it 'shows a disabled prefilled select if user is a member of 1 team' do
...@@ -94,7 +94,7 @@ feature 'Setup Mattermost slash commands', :js do ...@@ -94,7 +94,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost' click_link 'Add to Mattermost'
expect(page).to have_content('Select the team where the slash commands will be used in') expect(page).to have_content('Select the team where the slash commands will be used in')
expect(page).to have_content('The list shows all available teams.') expect(page).to have_content('The list shows all available teams that you are a member of.')
end end
it 'shows a select with team options user is a member of multiple teams' do it 'shows a select with team options user is a member of multiple teams' do
......
...@@ -62,7 +62,8 @@ describe 'Users > Terms' do ...@@ -62,7 +62,8 @@ describe 'Users > Terms' do
expect(current_path).to eq(project_issues_path(project)) expect(current_path).to eq(project_issues_path(project))
end end
it 'redirects back to the page the user was trying to save' do # Disabled until https://gitlab.com/gitlab-org/gitlab-ce/issues/37162 is solved properly
xit 'redirects back to the page the user was trying to save' do
visit new_project_issue_path(project) visit new_project_issue_path(project)
fill_in :issue_title, with: 'Hello world, a new issue' fill_in :issue_title, with: 'Hello world, a new issue'
......
require 'spec_helper'
require_relative '../../config/initializers/grape_route_helpers_fix'
describe 'route shadowing' do
include GrapeRouteHelpers::NamedRouteMatcher
it 'does not occur' do
path = api_v4_projects_merge_requests_path(id: 1)
expect(path).to eq('/api/v4/projects/1/merge_requests')
path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
expect(path).to eq('/api/v4/projects/1/merge_requests/3')
end
end
{
"env": {
"jasmine": true
},
"extends": "plugin:jasmine/recommended",
"globals": {
"appendLoadFixtures": false,
"appendLoadStyleFixtures": false,
"appendSetFixtures": false,
"appendSetStyleFixtures": false,
"getJSONFixture": false,
"loadFixtures": false,
"loadJSONFixtures": false,
"loadStyleFixtures": false,
"preloadFixtures": false,
"preloadStyleFixtures": false,
"readFixtures": false,
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
"spyOnDependency": false,
"spyOnEvent": false,
"ClassSpecHelper": false
},
"plugins": ["jasmine"],
"rules": {
"func-names": 0,
"jasmine/no-suite-dupes": [1, "branch"],
"jasmine/no-spec-dupes": [1, "branch"],
"no-console": 0,
"prefer-arrow-callback": 0
}
}
---
env:
jasmine: true
extends: plugin:jasmine/recommended
globals:
appendLoadFixtures: false
appendLoadStyleFixtures: false
appendSetFixtures: false
appendSetStyleFixtures: false
getJSONFixture: false
loadFixtures: false
loadJSONFixtures: false
loadStyleFixtures: false
preloadFixtures: false
preloadStyleFixtures: false
readFixtures: false
sandbox: false
setFixtures: false
setStyleFixtures: false
spyOnDependency: false
spyOnEvent: false
ClassSpecHelper: false
plugins:
- jasmine
rules:
func-names: off
jasmine/no-suite-dupes:
- warn
- branch
jasmine/no-spec-dupes:
- warn
- branch
no-console: off
prefer-arrow-callback: off
...@@ -84,9 +84,14 @@ describe('iPython notebook renderer', () => { ...@@ -84,9 +84,14 @@ describe('iPython notebook renderer', () => {
describe('error in JSON response', () => { describe('error in JSON response', () => {
let mock; let mock;
beforeEach((done) => { beforeEach(done => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet('/test').reply(() => Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' })); mock
.onGet('/test')
.reply(() =>
// eslint-disable-next-line prefer-promise-reject-errors
Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }),
);
renderNotebook(); renderNotebook();
......
/* global BoardService */
import Vue from 'vue'; import Vue from 'vue';
import '~/boards/stores/boards_store'; import '~/boards/stores/boards_store';
import BoardBlankState from '~/boards/components/board_blank_state.vue'; import BoardBlankState from '~/boards/components/board_blank_state.vue';
......
/* global List */ /* global List */
/* global ListAssignee */ /* global ListAssignee */
/* global ListLabel */ /* global ListLabel */
/* global BoardService */
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
......
/* global BoardService */
/* global List */ /* global List */
/* global ListIssue */ /* global ListIssue */
import Vue from 'vue'; import Vue from 'vue';
......
/* global BoardService */
/* global List */ /* global List */
import Vue from 'vue'; import Vue from 'vue';
......
/* eslint-disable comma-dangle, one-var, no-unused-vars */ /* eslint-disable comma-dangle, one-var, no-unused-vars */
/* global BoardService */
/* global ListIssue */ /* global ListIssue */
import Vue from 'vue'; import Vue from 'vue';
......
/* eslint-disable comma-dangle */ /* eslint-disable comma-dangle */
/* global BoardService */
/* global ListIssue */ /* global ListIssue */
import Vue from 'vue'; import Vue from 'vue';
......
/* eslint-disable comma-dangle */ /* eslint-disable comma-dangle */
/* global BoardService */
/* global List */ /* global List */
/* global ListIssue */ /* global ListIssue */
......
...@@ -75,10 +75,7 @@ describe('Commit pipeline status component', () => { ...@@ -75,10 +75,7 @@ describe('Commit pipeline status component', () => {
describe('When polling data was not succesful', () => { describe('When polling data was not succesful', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet('/dummy/endpoint').reply(() => { mock.onGet('/dummy/endpoint').reply(502, {});
const res = Promise.reject([502, { }]);
return res;
});
vm = new Component({ vm = new Component({
props: { props: {
endpoint: '/dummy/endpoint', endpoint: '/dummy/endpoint',
......
import Vue from 'vue'; const mountComponent = (Component, props = {}, el = null) =>
new Component({
const mountComponent = (Component, props = {}, el = null) => new Component({
propsData: props, propsData: props,
}).$mount(el); }).$mount(el);
export const createComponentWithStore = (Component, store, propsData = {}) => new Component({ export const createComponentWithStore = (Component, store, propsData = {}) =>
new Component({
store, store,
propsData, propsData,
});
export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => {
const Component = Vue.extend({
template,
mixins,
data() {
return props;
},
}); });
return mountComponent(Component, props);
};
export const mountComponentWithStore = (Component, { el, props, store }) => export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({ new Component({
store, store,
propsData: props || { }, propsData: props || {},
}).$mount(el); }).$mount(el);
export default mountComponent; export default mountComponent;
...@@ -8,10 +8,7 @@ describe('Confidential Issue Sidebar Block', () => { ...@@ -8,10 +8,7 @@ describe('Confidential Issue Sidebar Block', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(confidentialIssueSidebar); const Component = Vue.extend(confidentialIssueSidebar);
const service = { const service = {
update: () => new Promise((resolve, reject) => { update: () => Promise.resolve(true),
resolve(true);
reject('failed!');
}),
}; };
vm1 = new Component({ vm1 = new Component({
......
/* eslint-disable prefer-rest-params, wrap-iife, /* eslint-disable prefer-rest-params, wrap-iife,
no-unused-expressions, no-return-assign, no-param-reassign*/ no-unused-expressions, no-return-assign, no-param-reassign */
export default class MockU2FDevice { export default class MockU2FDevice {
constructor() { constructor() {
......
...@@ -19,6 +19,18 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -19,6 +19,18 @@ describe Gitlab::BitbucketImport::Importer do
] ]
end end
let(:reporters) do
[
nil,
{ "username" => "reporter1" },
nil,
{ "username" => "reporter2" },
{ "username" => "reporter1" },
nil,
{ "username" => "reporter3" }
]
end
let(:sample_issues_statuses) do let(:sample_issues_statuses) do
issues = [] issues = []
...@@ -36,6 +48,10 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -36,6 +48,10 @@ describe Gitlab::BitbucketImport::Importer do
} }
end end
reporters.map.with_index do |reporter, index|
issues[index]['reporter'] = reporter
end
issues issues
end end
...@@ -147,5 +163,19 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -147,5 +163,19 @@ describe Gitlab::BitbucketImport::Importer do
expect(importer.errors).to be_empty expect(importer.errors).to be_empty
end end
end end
describe 'issue import' do
it 'maps reporters to anonymous if bitbucket reporter is nil' do
allow(importer).to receive(:import_wiki)
importer.execute
expect(project.issues.size).to eq(7)
expect(project.issues.where("description LIKE ?", '%Anonymous%').size).to eq(3)
expect(project.issues.where("description LIKE ?", '%reporter1%').size).to eq(2)
expect(project.issues.where("description LIKE ?", '%reporter2%').size).to eq(1)
expect(project.issues.where("description LIKE ?", '%reporter3%').size).to eq(1)
expect(importer.errors).to be_empty
end
end
end end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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