summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml8
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/api.js10
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js20
-rw-r--r--app/assets/javascripts/boards/components/board.js8
-rw-r--r--app/assets/javascripts/boards/components/board_card.js3
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js33
-rw-r--r--app/assets/javascripts/boards/eventhub.js3
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js34
-rw-r--r--app/assets/javascripts/boards/models/list.js24
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js13
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js21
-rw-r--r--app/assets/javascripts/labels_select.js18
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/users_select.js5
-rw-r--r--app/assets/stylesheets/framework/filters.scss6
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb90
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml4
-rw-r--r--app/views/projects/diffs/_line.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml18
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml16
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml4
-rw-r--r--changelogs/unreleased/issue-boards-new-search-bar.yml4
-rw-r--r--config/gitlab.yml.example10
-rw-r--r--config/initializers/8_metrics.rb6
-rw-r--r--config/initializers/omniauth.rb9
-rw-r--r--db/post_migrate/20170309171644_reset_relative_position_for_issue.rb17
-rw-r--r--doc/ci/docker/using_docker_images.md10
-rw-r--r--doc/development/frontend.md34
-rw-r--r--doc/system_hooks/system_hooks.md15
-rw-r--r--doc/update/8.16-to-8.17.md14
-rw-r--r--doc/update/8.17-to-9.0.md13
-rw-r--r--features/support/capybara.rb9
-rw-r--r--lib/gitlab/diff/line.rb6
-rw-r--r--lib/gitlab/diff/parser.rb6
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/ldap/user.rb2
-rw-r--r--lib/omni_auth/strategies/bitbucket.rb (renamed from lib/omniauth/strategies/bitbucket.rb)0
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb167
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb2
-rw-r--r--spec/javascripts/project_title_spec.js2
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb48
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb120
-rw-r--r--spec/support/capybara.rb9
52 files changed, 576 insertions, 298 deletions
diff --git a/.flayignore b/.flayignore
index fc64b0b5892..47597025115 100644
--- a/.flayignore
+++ b/.flayignore
@@ -2,3 +2,4 @@
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
app/policies/project_policy.rb
+app/models/concerns/relative_positioning.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index db3d25195dc..492f5ef715d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -67,9 +67,11 @@ stages:
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
+ when: always
paths:
- - knapsack/
- coverage/
+ - knapsack/
+ - tmp/capybara/
.spinach-knapsack: &spinach-knapsack
stage: test
@@ -85,9 +87,11 @@ stages:
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
+ when: always
paths:
- - knapsack/
- coverage/
+ - knapsack/
+ - tmp/capybara/
# Prepare and merge knapsack tests
diff --git a/Gemfile.lock b/Gemfile.lock
index c60c045a4c2..e38f45b8e98 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -304,7 +304,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
- google-protobuf (3.2.0)
+ google-protobuf (3.2.0.1)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index a0946eb392a..e5f36c84987 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -51,15 +51,15 @@ var Api = {
});
},
// Return projects list. Filtered by query
- projects: function(query, order, callback) {
+ projects: function(query, options, callback) {
var url = Api.buildUrl(Api.projectsPath);
return $.ajax({
url: url,
- data: {
+ data: $.extend({
search: query,
- order_by: order,
- per_page: 20
- },
+ per_page: 20,
+ membership: true
+ }, options),
dataType: "json"
}).done(function(projects) {
return callback(projects);
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 55d13be6e5f..3874c2819a5 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -2,6 +2,9 @@
/* global Vue */
/* global BoardService */
+import FilteredSearchBoards from './filtered_search_boards';
+import eventHub from './eventhub';
+
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
require('./models/issue');
@@ -59,6 +62,14 @@ $(() => {
},
created () {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
+
+ this.filterManager = new FilteredSearchBoards(Store.filter, true);
+
+ // Listen for updateTokens event
+ eventHub.$on('updateTokens', this.updateTokens);
+ },
+ beforeDestroy() {
+ eventHub.$off('updateTokens', this.updateTokens);
},
mounted () {
Store.disabled = this.disabled;
@@ -77,11 +88,16 @@ $(() => {
Store.addBlankState();
this.loading = false;
});
- }
+ },
+ methods: {
+ updateTokens() {
+ this.filterManager.updateTokens();
+ }
+ },
});
gl.IssueBoardsSearch = new Vue({
- el: document.getElementById('js-boards-search'),
+ el: document.getElementById('js-add-list'),
data: {
filters: Store.state.filters
},
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 18324de18b3..30d3be453be 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -28,16 +28,16 @@ require('./board_list');
data () {
return {
detailIssue: Store.detail,
- filters: Store.state.filters,
+ filter: Store.filter,
};
},
watch: {
- filters: {
- handler () {
+ filter: {
+ handler() {
this.list.page = 1;
this.list.getIssues(true);
},
- deep: true
+ deep: true,
},
detailIssue: {
handler () {
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
index 795b3cf2ec0..4b72090df31 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -17,7 +17,8 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
- :root-path="rootPath" />
+ :root-path="rootPath"
+ :update-filters="true" />
</li>
`,
components: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 22a8b971ff8..69e30cec4c5 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,4 +1,6 @@
/* global Vue */
+import eventHub from '../eventhub';
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -23,6 +25,11 @@
type: String,
required: true,
},
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
methods: {
showLabel(label) {
@@ -31,29 +38,25 @@
return !this.list.label || label.id !== this.list.label.id;
},
filterByLabel(label, e) {
- let labelToggleText = label.title;
- const labelIndex = Store.state.filters.label_name.indexOf(label.title);
+ if (!this.updateFilters) return;
+
+ const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+ const labelTitle = encodeURIComponent(label.title);
+ const param = `label_name[]=${labelTitle}`;
+ const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
- Store.state.filters.label_name.push(label.title);
- $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ filterPath.push(param);
} else {
- Store.state.filters.label_name.splice(labelIndex, 1);
- labelToggleText = Store.state.filters.label_name[0];
- $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ filterPath.splice(labelIndex, 1);
}
- const selectedLabels = Store.state.filters.label_name;
- if (selectedLabels.length === 0) {
- labelToggleText = 'Label';
- } else if (selectedLabels.length > 1) {
- labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
- }
-
- $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
},
labelStyle(label) {
return {
diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/boards/eventhub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
new file mode 100644
index 00000000000..47448b02bdd
--- /dev/null
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -0,0 +1,34 @@
+export default class FilteredSearchBoards extends gl.FilteredSearchManager {
+ constructor(store, updateUrl = false) {
+ super('boards');
+
+ this.store = store;
+ this.updateUrl = updateUrl;
+
+ // Issue boards is slightly different, we handle all the requests async
+ // instead or reloading the page, we just re-fire the list ajax requests
+ this.isHandledAsync = true;
+ }
+
+ updateObject(path) {
+ this.store.path = path.substr(1);
+
+ if (this.updateUrl) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
+ }
+
+ updateTokens() {
+ const tokens = document.querySelectorAll('.js-visual-token');
+
+ // Remove all the tokens as they will be replaced by the search manager
+ [].forEach.call(tokens, (el) => {
+ el.parentNode.removeChild(el);
+ });
+
+ this.loadSearchParamsFromURL();
+
+ // Get the placeholder back if search is empty
+ this.filteredSearchInput.dispatchEvent(new Event('input'));
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index f237567208c..3251ca76b26 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -10,7 +10,6 @@ class List {
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['done', 'blank'].indexOf(this.type) > -1;
- this.filters = gl.issueBoards.BoardsStore.state.filters;
this.page = 1;
this.loading = true;
this.loadingMore = false;
@@ -65,12 +64,27 @@ class List {
}
getIssues (emptyIssues = true) {
- const filters = this.filters;
- const data = { page: this.page };
+ const data = gl.issueBoards.BoardsStore.filter.path.split('&').reduce((data, filterParam) => {
+ if (filterParam === '') return data;
+ const paramSplit = filterParam.split('=');
+ const paramKeyNormalized = paramSplit[0].replace('[]', '');
+ const isArray = paramSplit[0].indexOf('[]');
+ const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
+
+ if (isArray !== -1) {
+ if (!data[paramKeyNormalized]) {
+ data[paramKeyNormalized] = [];
+ }
+
+ data[paramKeyNormalized].push(value);
+ } else {
+ data[paramKeyNormalized] = value;
+ }
- Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
+ return data;
+ }, { page: this.page });
- if (this.label) {
+ if (this.label && data.label_name) {
data.label_name = data.label_name.filter(label => label !== this.label.title);
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 3866c6bbfc6..28ecb322df7 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,9 @@
gl.issueBoards.BoardsStore = {
disabled: false,
+ filter: {
+ path: '',
+ },
state: {},
detail: {
issue: {}
@@ -18,13 +21,7 @@
},
create () {
this.state.lists = [];
- this.state.filters = {
- author_id: gl.utils.getParameterValues('author_id')[0],
- assignee_id: gl.utils.getParameterValues('assignee_id')[0],
- milestone_title: gl.utils.getParameterValues('milestone_title')[0],
- label_name: gl.utils.getParameterValues('label_name[]'),
- search: ''
- };
+ this.filter.path = gl.utils.getUrlParamsArray().join('&');
},
addList (listObj) {
const list = new List(listObj);
@@ -123,7 +120,7 @@
})[0];
},
updateFiltersUrl () {
- history.pushState(null, null, `?${$.param(this.state.filters)}`);
+ history.pushState(null, null, `?${this.filter.path}`);
}
};
})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 835e87a28d7..f885932bd91 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -106,8 +106,15 @@
e.preventDefault();
if (!activeElements.length) {
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
+ if (this.isHandledAsync) {
+ e.stopImmediatePropagation();
+
+ this.filteredSearchInput.blur();
+ this.dropdownManager.resetDropdowns();
+ } else {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
+ }
this.search();
}
@@ -200,6 +207,10 @@
this.handleInputPlaceholder();
this.dropdownManager.resetDropdowns();
+
+ if (this.isHandledAsync) {
+ this.search();
+ }
}
handleInputVisualToken() {
@@ -346,7 +357,11 @@
const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
- gl.utils.visitUrl(parameterizedUrl);
+ if (this.updateObject) {
+ this.updateObject(parameterizedUrl);
+ } else {
+ gl.utils.visitUrl(parameterizedUrl);
+ }
}
getUsernameParams() {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9e2d14c7f87..c648a0f076c 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -353,31 +353,17 @@
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
- !$dropdown.closest('.add-issues-modal').length) {
- boardsModel = gl.issueBoards.BoardsStore.state.filters;
- } else if ($dropdown.closest('.add-issues-modal').length) {
+ if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = gl.issueBoards.ModalStore.store.filter;
}
if (boardsModel) {
if (label.isAny) {
boardsModel['label_name'] = [];
- }
- else if ($el.hasClass('is-active')) {
+ } else if ($el.hasClass('is-active')) {
boardsModel['label_name'].push(label.title);
}
- else {
- var filters = boardsModel['label_name'];
- filters = filters.filter(function (filteredLabel) {
- return filteredLabel !== label.title;
- });
- boardsModel['label_name'] = filters;
- }
- if (!$dropdown.closest('.add-issues-modal').length) {
- gl.issueBoards.BoardsStore.updateFiltersUrl();
- }
e.preventDefault();
return;
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 51fa5c828b3..4c4f94cb9f3 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -124,18 +124,12 @@
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
- !$dropdown.closest('.add-issues-modal').length) {
- boardsStore = gl.issueBoards.BoardsStore.state.filters;
- } else if ($dropdown.closest('.add-issues-modal').length) {
+ if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
- if (!$dropdown.closest('.add-issues-modal').length) {
- gl.issueBoards.BoardsStore.updateFiltersUrl();
- }
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index f80e765ce30..3c1c1e7dceb 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -35,7 +35,7 @@
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
- return Api.projects(term, orderBy, projectsCallback);
+ return Api.projects(term, { order_by: orderBy }, projectsCallback);
}
},
url: function(project) {
@@ -84,7 +84,7 @@
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else {
- return Api.projects(query.term, _this.orderBy, projectsCallback);
+ return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback);
}
};
})(this),
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index e66418beeab..15f5963353a 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -47,7 +47,7 @@
fields: ['name']
},
data: function(term, callback) {
- return Api.projects(term, 'id', function(data) {
+ return Api.projects(term, { order_by: 'id' }, function(data) {
data.unshift({
name_with_namespace: 'Any'
});
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 27af859f7d8..c7a57b47834 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -217,11 +217,6 @@
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
- } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
- selectedId = user.id;
- gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
- gl.issueBoards.BoardsStore.updateFiltersUrl();
- e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form'));
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 8f2150066c7..dd2daa4b872 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -156,7 +156,6 @@
width: 100%;
border: 1px solid $border-color;
background-color: $white-light;
- max-width: 87%;
@media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%;
@@ -219,6 +218,11 @@
}
}
+.filter-dropdown-container {
+ display: -webkit-flex;
+ display: flex;
+}
+
.dropdown-menu .filter-dropdown-item {
padding: 0;
}
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 2d8064c9878..8b6c83d4fed 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,5 +1,3 @@
-require 'project_wiki'
-
class Projects::WikisController < Projects::ApplicationController
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 603f2dd7e5d..f1d8532a6d6 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -2,16 +2,14 @@ module RelativePositioning
extend ActiveSupport::Concern
MIN_POSITION = 0
+ START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
+ IDEAL_DISTANCE = 500
included do
after_save :save_positionable_neighbours
end
- def min_relative_position
- self.class.in_projects(project.id).minimum(:relative_position)
- end
-
def max_relative_position
self.class.in_projects(project.id).maximum(:relative_position)
end
@@ -26,7 +24,7 @@ module RelativePositioning
maximum(:relative_position)
end
- prev_pos || MIN_POSITION
+ prev_pos
end
def next_relative_position
@@ -39,55 +37,95 @@ module RelativePositioning
minimum(:relative_position)
end
- next_pos || MAX_POSITION
+ next_pos
end
def move_between(before, after)
return move_after(before) unless after
return move_before(after) unless before
+ # If there is no place to insert an issue we need to create one by moving the before issue closer
+ # to its predecessor. This process will recursively move all the predecessors until we have a place
+ if (after.relative_position - before.relative_position) < 2
+ before.move_before
+ @positionable_neighbours = [before]
+ end
+
+ self.relative_position = position_between(before.relative_position, after.relative_position)
+ end
+
+ def move_after(before = self)
pos_before = before.relative_position
+ pos_after = before.next_relative_position
+
+ if before.shift_after?
+ issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
+ issue_to_move.move_after
+ @positionable_neighbours = [issue_to_move]
+
+ pos_after = issue_to_move.relative_position
+ end
+
+ self.relative_position = position_between(pos_before, pos_after)
+ end
+
+ def move_before(after = self)
pos_after = after.relative_position
+ pos_before = after.prev_relative_position
- if pos_after && (pos_before == pos_after)
- self.relative_position = pos_before
- before.move_before(self)
- after.move_after(self)
+ if after.shift_before?
+ issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
+ issue_to_move.move_before
+ @positionable_neighbours = [issue_to_move]
- @positionable_neighbours = [before, after]
- else
- self.relative_position = position_between(pos_before, pos_after)
+ pos_before = issue_to_move.relative_position
end
+
+ self.relative_position = position_between(pos_before, pos_after)
end
- def move_before(after)
- self.relative_position = position_between(after.prev_relative_position, after.relative_position)
+ def move_to_end
+ self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
end
- def move_after(before)
- self.relative_position = position_between(before.relative_position, before.next_relative_position)
+ # Indicates if there is an issue that should be shifted to free the place
+ def shift_after?
+ next_pos = next_relative_position
+ next_pos && (next_pos - relative_position) == 1
end
- def move_to_end
- self.relative_position = position_between(max_relative_position, MAX_POSITION)
+ # Indicates if there is an issue that should be shifted to free the place
+ def shift_before?
+ prev_pos = prev_relative_position
+ prev_pos && (relative_position - prev_pos) == 1
end
private
# This method takes two integer values (positions) and
- # calculates some random position between them. The range is huge as
- # the maximum integer value is 2147483647. Ideally, the calculated value would be
- # exactly between those terminating values, but this will introduce possibility of a race condition
- # so two or more issues can get the same value, we want to avoid that and we also want to avoid
- # using a lock here. If we have two issues with distance more than one thousand, we are OK.
- # Given the huge range of possible values that integer can fit we shoud never face a problem.
+ # calculates the position between them. The range is huge as
+ # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
+ # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number
def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION
pos_before, pos_after = [pos_before, pos_after].sort
- rand(pos_before.next..pos_after.pred)
+ halfway = (pos_after + pos_before) / 2
+ distance_to_halfway = pos_after - halfway
+
+ if distance_to_halfway < IDEAL_DISTANCE
+ halfway
+ else
+ if pos_before == MIN_POSITION
+ pos_after - IDEAL_DISTANCE
+ elsif pos_after == MAX_POSITION
+ pos_before + IDEAL_DISTANCE
+ else
+ halfway
+ end
+ end
end
def save_positionable_neighbours
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 87f9b503989..1fb2c6271ad 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -410,7 +410,7 @@
:javascript
$('#js-project-dropdown').glDropdown({
data: function (term, callback) {
- Api.projects(term, "last_activity_at", function (data) {
+ Api.projects(term, { order_by: 'last_activity_at' }, function (data) {
callback(data);
});
},
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 3ae78387938..fa463edd526 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -4,6 +4,7 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('filtered_search')
= page_specific_javascript_bundle_tag('boards')
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
@@ -12,7 +13,8 @@
= render "projects/issues/head"
-= render 'shared/issuable/filter', type: :boards
+.hidden-xs.hidden-sm
+ = render 'shared/issuable/search_bar', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 62135d3ae32..c09c7b87e24 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -9,7 +9,7 @@
- case type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index e7758c8bdfa..b7346f27ddb 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -12,7 +12,7 @@
- case left.type
- when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.line_content.match= left.text
- else
@@ -31,7 +31,7 @@
- case right.type
- when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num
%td.line_content.match= right.text
- else
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index f17ae9f28eb..f0bad69a989 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,4 +1,4 @@
-- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
+- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters
@@ -34,21 +34,7 @@
%a{ href: page_filter_path(without: issuable_filter_params) } Reset filters
.pull-right
- - if boards_page
- #js-boards-search.issue-boards-search
- %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- - if can?(current_user, :admin_list, @project)
- #js-add-issues-btn.pull-right.prepend-left-10
- .dropdown.pull-right
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
- Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, @project)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
- - else
- = render 'shared/sort_dropdown'
+ = render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f8123846596..f1730b1791c 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -85,8 +85,20 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
- .pull-right.filter-dropdown-container
- = render 'shared/sort_dropdown'
+ .filter-dropdown-container
+ - if type == :boards
+ - if can?(current_user, :admin_list, @project)
+ .dropdown.prepend-left-10#js-add-list
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
+ Add list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
+ - if can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
+ #js-add-issues-btn.prepend-left-10
+ - else
+ = render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 4a27965754d..7e9fb7bb4d3 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -7,7 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
-- cache_key.push(project.commit.status) if project.commit.try(:status)
+- cache_key.push(project.commit&.sha, project.commit&.status)
%li.project-row{ class: css_class }
= cache(cache_key) do
diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
new file mode 100644
index 00000000000..4db684c40b2
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve project pipeline status caching problem on dashboard
+merge_request: 9895
+author:
diff --git a/changelogs/unreleased/issue-boards-new-search-bar.yml b/changelogs/unreleased/issue-boards-new-search-bar.yml
new file mode 100644
index 00000000000..b02be70c470
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-new-search-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Added new filtered search bar to issue boards
+merge_request:
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 2bc39ea3f65..954809a882c 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -456,14 +456,6 @@ production: &base
# 4. Advanced settings
# ==========================
- # GitLab Satellites
- #
- # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at
- # least. This setting is fed to 'rm -rf' in
- # db/migrate/20151023144219_remove_satellites.rb
- satellites:
- path: /home/git/gitlab-satellites/
-
## Repositories settings
repositories:
# Paths where repositories can be stored. Give the canonicalized absolute pathname.
@@ -581,8 +573,6 @@ test:
# In order to setup it correctly you need to specify
# your system username you use to run GitLab
# user: YOUR_USERNAME
- satellites:
- path: tmp/tests/gitlab-satellites/
repositories:
storages:
default:
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index 3e1657b8382..5e0eefdb154 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -124,9 +124,9 @@ if Gitlab::Metrics.enabled?
# These are manually require'd so the classes are registered properly with
# ActiveSupport.
- require 'gitlab/metrics/subscribers/action_view'
- require 'gitlab/metrics/subscribers/active_record'
- require 'gitlab/metrics/subscribers/rails_cache'
+ require_dependency 'gitlab/metrics/subscribers/action_view'
+ require_dependency 'gitlab/metrics/subscribers/active_record'
+ require_dependency 'gitlab/metrics/subscribers/rails_cache'
Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Metrics::RackMiddleware)
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index ab5a0561b8c..f7fa6d1c2de 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -20,15 +20,12 @@ OmniAuth.config.before_request_phase do |env|
end
if Gitlab.config.omniauth.enabled
- Gitlab.config.omniauth.providers.each do |provider|
- if provider['name'] == 'kerberos'
- require 'omniauth-kerberos'
- end
- end
+ provider_names = Gitlab.config.omniauth.providers.map(&:name)
+ require 'omniauth-kerberos' if provider_names.include?('kerberos')
end
module OmniAuth
module Strategies
- autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
+ autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket')
end
end
diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
new file mode 100644
index 00000000000..b61dd7cfc61
--- /dev/null
+++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetRelativePositionForIssue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:issues, :relative_position, nil) do |table, query|
+ query.where(table[:relative_position].not_eq(nil))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 00787323b6b..f025a7e3496 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -170,13 +170,17 @@ services:
```
When the job is run, `tutum/wordpress` will be started and you will have
-access to it from your build container under the hostname `tutum__wordpress`.
+access to it from your build container under the hostnames `tutum-wordpress`
+(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`.
-The alias hostname for the service is made from the image name following these
+*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.*
+
+The alias hostnames for the service are made from the image name following these
rules:
1. Everything after `:` is stripped
-2. Slash (`/`) is replaced with double underscores (`__`)
+2. Slash (`/`) is replaced with double underscores (`__`) - primary alias
+3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer
## Configuring services
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index d646de7c54a..e7add17fe2d 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -66,6 +66,8 @@ Let's look into each of them:
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
+The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
+
Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
@@ -86,7 +88,7 @@ You can read more about components in Vue.js site, [Component System][component-
**A folder for the Store**
-The Store is a simple object that allows us to manage the state in a single
+The Store is a class that allows us to manage the state in a single
source of truth.
The concept we are trying to follow is better explained by Vue documentation
@@ -289,7 +291,7 @@ When exactly one object is needed for a given task, prefer to define it as a
`class` rather than as an object literal. Prefer also to explicitly restrict
instantiation, unless flexibility is important (e.g. for testing).
-```
+```javascript
// bad
gl.MyThing = {
@@ -332,6 +334,33 @@ gl.MyThing = MyThing;
```
+### Manipulating the DOM in a JS Class
+
+When writing a class that needs to manipulate the DOM guarantee a container option is provided.
+This is useful when we need that class to be instantiated more than once in the same page.
+
+Bad:
+```javascript
+class Foo {
+ constructor() {
+ document.querySelector('.bar');
+ }
+}
+new Foo();
+```
+
+Good:
+```javascript
+class Foo {
+ constructor(opts) {
+ document.querySelector(`${opts.container} .bar`);
+ }
+}
+
+new Foo({ container: '.my-element' });
+```
+You can find an example of the above in this [class][container-class-example];
+
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
@@ -457,3 +486,4 @@ Scenario: Developer can approve merge request
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
[team-page]: https://about.gitlab.com/team
[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js
+[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index ec13c2446ef..ad5ffc84473 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -313,8 +313,19 @@ X-Gitlab-Event: System Hook
"git_ssh_url":"git@example.com:mike/diaspora.git",
"visibility_level":0
},
- "commits": [],
- "total_commits_count": 0
+ "commits": [
+ {
+ "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "message": "Add simple search to projects in public area",
+ "timestamp": "2013-05-13T18:18:08+00:00",
+ "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
}
```
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 954109ba18f..74ffe0bc846 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -139,7 +139,7 @@ sudo -u git -H git checkout v4.1.1
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+There might be new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
cd /home/git/gitlab
@@ -195,6 +195,16 @@ See [smtp_settings.rb.sample] as an example.
#### Init script
+There might be new configuration options available for [`gitlab.default.example`][gl-example].
+You need to update this file if you want to [enable GitLab Pages][pages-admin].
+View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-16-stable:lib/support/init.d/gitlab.default.example origin/8-17-stable:lib/support/init.d/gitlab.default.example
+```
+
Ensure you're still up-to-date with the latest init script changes:
```bash
@@ -254,3 +264,5 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example
+[pages-admin]: ../administration/pages/source.md
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 1fe38cf8d2a..626507c0482 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -149,7 +149,7 @@ sudo -u git -H git checkout v5.0.0
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
cd /home/git/gitlab
@@ -189,7 +189,7 @@ Update your current configuration as follows, replacing with your storages names
**For Omnibus installations**
-1. Upate your `/etc/gitlab/gitlab.rb`, from
+1. Update your `/etc/gitlab/gitlab.rb`, from
```ruby
git_data_dirs({
@@ -260,6 +260,14 @@ See [smtp_settings.rb.sample] as an example.
#### Init script
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-17-stable:lib/support/init.d/gitlab.default.example origin/9-0-stable:lib/support/init.d/gitlab.default.example
+```
+
Ensure you're still up-to-date with the latest init script changes:
```bash
@@ -319,3 +327,4 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index a5fcbb65131..c0c489d2775 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,5 +1,6 @@
require 'spinach/capybara'
require 'capybara/poltergeist'
+require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
@@ -20,12 +21,8 @@ end
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = false
-unless ENV['CI'] || ENV['CI_SERVER']
- require 'capybara-screenshot/spinach'
-
- # Keep only the screenshots generated from the last failing test suite
- Capybara::Screenshot.prune_strategy = :keep_last_run
-end
+# Keep only the screenshots generated from the last failing test suite
+Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 80a146b4a5a..114656958e3 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -38,11 +38,11 @@ module Gitlab
end
def added?
- type == 'new'
+ type == 'new' || type == 'new-nonewline'
end
def removed?
- type == 'old'
+ type == 'old' || type == 'old-nonewline'
end
def rich_text
@@ -52,7 +52,7 @@ module Gitlab
end
def meta?
- type == 'match' || type == 'nonewline'
+ type == 'match'
end
def as_json(opts = nil)
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 8f844224a7a..742f989c50b 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -11,6 +11,7 @@ module Gitlab
line_old = 1
line_new = 1
type = nil
+ context = nil
# By returning an Enumerator we make it possible to search for a single line (with #find)
# without having to instantiate all the others that come after it.
@@ -31,7 +32,8 @@ module Gitlab
line_obj_index += 1
next
elsif line[0] == '\\'
- type = 'nonewline'
+ type = "#{context}-nonewline"
+
yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_obj_index += 1
else
@@ -43,8 +45,10 @@ module Gitlab
case line[0]
when "+"
line_new += 1
+ context = :new
when "-"
line_old += 1
+ context = :old
when "\\" # rubocop:disable Lint/EmptyWhen
# No increment
else
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 6c275a8d5de..5ab84266b7d 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -1,7 +1,7 @@
module Gitlab
module GonHelper
def add_gon_variables
- gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen"
+ gon.api_version = 'v4'
gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
gon.asset_host = ActionController::Base.asset_host
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index b84c81f1a6c..2d5e47a6f3b 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -1,5 +1,3 @@
-require 'gitlab/o_auth/user'
-
# LDAP extension for User model
#
# * Find or create user from omniauth.auth data
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb
index 5a7d67c2390..5a7d67c2390 100644
--- a/lib/omniauth/strategies/bitbucket.rb
+++ b/lib/omni_auth/strategies/bitbucket.rb
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index a3e24bb5ffa..f7f2d883d2f 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -51,7 +51,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
end
it 'does not show tooltip on add issues button' do
- button = page.find('.issue-boards-search button', text: 'Add issues')
+ button = page.find('.filter-dropdown-container button', text: 'Add issues')
expect(button[:title]).not_to eq("Please add a list to your board first")
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index ecc356f2505..f7e8b78b54d 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -29,7 +29,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'shows tooltip on add issues button' do
- button = page.find('.issue-boards-search button', text: 'Add issues')
+ button = page.find('.filter-dropdown-container button', text: 'Add issues')
expect(button[:"data-original-title"]).to eq("Please add a list to your board first")
end
@@ -115,9 +115,8 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'search done list' do
- page.within('#js-boards-search') do
- find('.form-control').set(issue8.title)
- end
+ find('.filtered-search').set(issue8.title)
+ find('.filtered-search').native.send_keys(:enter)
wait_for_vue_resource
@@ -127,9 +126,8 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'search list' do
- page.within('#js-boards-search') do
- find('.form-control').set(issue5.title)
- end
+ find('.filtered-search').set(issue5.title)
+ find('.filtered-search').native.send_keys(:enter)
wait_for_vue_resource
@@ -333,7 +331,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(find('.issue-boards-search')).to have_selector('.open')
+ expect(page).to have_css('#js-add-list.open')
end
it 'creates new list from a new label' do
@@ -359,17 +357,9 @@ describe 'Issue Boards', feature: true, js: true do
context 'filtering' do
it 'filters by author' do
- page.within '.issues-filters' do
- click_button('Author')
- wait_for_ajax
-
- page.within '.dropdown-menu-author' do
- click_link(user2.name)
- end
- wait_for_vue_resource
-
- expect(find('.js-author-search')).to have_content(user2.name)
- end
+ set_filter("author", user2.username)
+ click_filter_link(user2.username)
+ submit_filter
wait_for_vue_resource
wait_for_board_cards(1, 1)
@@ -377,17 +367,9 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by assignee' do
- page.within '.issues-filters' do
- click_button('Assignee')
- wait_for_ajax
-
- page.within '.dropdown-menu-assignee' do
- click_link(user.name)
- end
- wait_for_vue_resource
-
- expect(find('.js-assignee-search')).to have_content(user.name)
- end
+ set_filter("assignee", user.username)
+ click_filter_link(user.username)
+ submit_filter
wait_for_vue_resource
@@ -396,17 +378,9 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by milestone' do
- page.within '.issues-filters' do
- click_button('Milestone')
- wait_for_ajax
-
- page.within '.milestone-filter' do
- click_link(milestone.title)
- end
- wait_for_vue_resource
-
- expect(find('.js-milestone-select')).to have_content(milestone.title)
- end
+ set_filter("milestone", "\"#{milestone.title}\"")
+ click_filter_link(milestone.title)
+ submit_filter
wait_for_vue_resource
wait_for_board_cards(1, 1)
@@ -415,16 +389,9 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by label' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
wait_for_vue_resource
wait_for_board_cards(1, 1)
@@ -432,19 +399,14 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by label with space after reload' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(accepting.title)
- wait_for_vue_resource(spinner: false)
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", "\"#{accepting.title}\"")
+ click_filter_link(accepting.title)
+ submit_filter
# Test after reload
page.evaluate_script 'window.location.reload()'
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..3))
wait_for_vue_resource
@@ -460,26 +422,16 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'removes filtered labels' do
- wait_for_vue_resource
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
- page.within '.labels-filter' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource(spinner: false)
- end
-
- expect(page).to have_css('input[name="label_name[]"]', visible: false)
+ wait_for_board_cards(1, 1)
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource(spinner: false)
- end
+ find('.clear-search').click
+ submit_filter
- expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
- end
+ wait_for_board_cards(1, 8)
end
it 'infinite scrolls list with label filter' do
@@ -487,16 +439,9 @@ describe 'Issue Boards', feature: true, js: true do
create(:labeled_issue, project: project, labels: [planning, testing])
end
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
wait_for_vue_resource
@@ -518,18 +463,13 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by multiple labels' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
- page.within(find('.dropdown-menu-labels')) do
- click_link(testing.title)
- wait_for_vue_resource
- click_link(bug.title)
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", bug.title)
+ click_filter_link(bug.title)
+
+ submit_filter
wait_for_vue_resource
@@ -545,14 +485,14 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
+ page.within('.tokens-container') do
+ expect(page).to have_content(bug.title)
+ end
+
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..3))
-
- page.within('.labels-filter') do
- expect(find('.dropdown-toggle-text')).to have_content(bug.title)
- end
end
it 'removes label filter by clicking label button on issue' do
@@ -560,16 +500,13 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.card', match: :first)) do
click_button(bug.title)
end
+
wait_for_vue_resource
expect(page).to have_selector('.card', count: 1)
end
wait_for_vue_resource
-
- page.within('.labels-filter') do
- expect(find('.dropdown-toggle-text')).to have_content(bug.title)
- end
end
end
end
@@ -643,4 +580,20 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_board_cards(board, 0)
end
end
+
+ def set_filter(type, text)
+ find('.filtered-search').native.send_keys("#{type}:#{text}")
+ end
+
+ def submit_filter
+ find('.filtered-search').native.send_keys(:enter)
+ end
+
+ def click_filter_link(link_text)
+ page.within('.filtered-search-input-container') do
+ expect(page).to have_button(link_text)
+
+ click_button(link_text)
+ end
+ end
end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 73553f97d6f..bfe43bff10f 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -176,7 +176,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
end
def selected_sort_order
- find('.pull-right .dropdown button').text.downcase
+ find('.filter-dropdown-container .dropdown button').text.downcase
end
def visit_merge_requests_with_state(project, state)
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 69d9587771f..3a1d4e2440f 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -26,7 +26,7 @@ require('~/project');
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
- expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 });
+ expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true });
d = $.Deferred();
d.resolve(this.projects_data);
return d.promise();
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index b983d73f8be..e76128ecd87 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -91,6 +91,54 @@ eos
end
end
+ describe '\ No newline at end of file' do
+ it "parses nonewline in one file correctly" do
+ first_nonewline_diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ +ipsum
+ lorem
+ -ipsum
+ \\ No newline at end of file
+ END
+ lines = parser.parse(first_nonewline_diff.lines).to_a
+
+ expect(lines[0].type).to eq('new')
+ expect(lines[0].text).to eq('+ipsum')
+ expect(lines[2].type).to eq('old')
+ expect(lines[3].type).to eq('old-nonewline')
+ expect(lines[1].old_pos).to eq(1)
+ expect(lines[1].new_pos).to eq(2)
+ end
+
+ it "parses nonewline in two files correctly" do
+ both_nonewline_diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ -lorem
+ -ipsum
+ \\ No newline at end of file
+ +ipsum
+ +lorem
+ \\ No newline at end of file
+ END
+ lines = parser.parse(both_nonewline_diff.lines).to_a
+
+ expect(lines[0].type).to eq('old')
+ expect(lines[1].type).to eq('old')
+ expect(lines[2].type).to eq('old-nonewline')
+ expect(lines[5].type).to eq('new-nonewline')
+ expect(lines[3].text).to eq('+ipsum')
+ expect(lines[3].old_pos).to eq(3)
+ expect(lines[3].new_pos).to eq(1)
+ expect(lines[4].text).to eq('+lorem')
+ expect(lines[4].old_pos).to eq(3)
+ expect(lines[4].new_pos).to eq(2)
+ end
+ end
+
context 'when lines is empty' do
it { expect(parser.parse([])).to eq([]) }
it { expect(parser.parse(nil)).to eq([]) }
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
index 69906382545..255b584a85e 100644
--- a/spec/models/concerns/relative_positioning_spec.rb
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -12,12 +12,6 @@ describe Issue, 'RelativePositioning' do
end
end
- describe '#min_relative_position' do
- it 'returns maximum position' do
- expect(issue.min_relative_position).to eq issue.relative_position
- end
- end
-
describe '#max_relative_position' do
it 'returns maximum position' do
expect(issue.max_relative_position).to eq issue1.relative_position
@@ -29,8 +23,8 @@ describe Issue, 'RelativePositioning' do
expect(issue1.prev_relative_position).to eq issue.relative_position
end
- it 'returns minimum position if there is no issue above' do
- expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION
+ it 'returns nil if there is no issue above' do
+ expect(issue.prev_relative_position).to eq nil
end
end
@@ -39,8 +33,8 @@ describe Issue, 'RelativePositioning' do
expect(issue.next_relative_position).to eq issue1.relative_position
end
- it 'returns next position if there is no issue below' do
- expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION
+ it 'returns nil if there is no issue below' do
+ expect(issue1.next_relative_position).to eq nil
end
end
@@ -72,6 +66,34 @@ describe Issue, 'RelativePositioning' do
end
end
+ describe '#shift_after?' do
+ it 'returns true' do
+ issue.update(relative_position: issue1.relative_position - 1)
+
+ expect(issue.shift_after?).to be_truthy
+ end
+
+ it 'returns false' do
+ issue.update(relative_position: issue1.relative_position - 2)
+
+ expect(issue.shift_after?).to be_falsey
+ end
+ end
+
+ describe '#shift_before?' do
+ it 'returns true' do
+ issue.update(relative_position: issue1.relative_position + 1)
+
+ expect(issue.shift_before?).to be_truthy
+ end
+
+ it 'returns false' do
+ issue.update(relative_position: issue1.relative_position + 2)
+
+ expect(issue.shift_before?).to be_falsey
+ end
+ end
+
describe '#move_between' do
it 'positions issue between two other' do
new_issue.move_between(issue, issue1)
@@ -100,5 +122,83 @@ describe Issue, 'RelativePositioning' do
expect(new_issue.relative_position).to be > issue.relative_position
expect(issue.relative_position).to be < issue1.relative_position
end
+
+ it 'positions issues between other two if distance is 1' do
+ issue1.update relative_position: issue.relative_position + 1
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'positions issue in the middle of other two if distance is big enough' do
+ issue.update relative_position: 6000
+ issue1.update relative_position: 10000
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to eq(8000)
+ end
+
+ it 'positions issue closer to the middle if we are at the very top' do
+ issue1.update relative_position: 6000
+
+ new_issue.move_between(nil, issue1)
+
+ expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE)
+ end
+
+ it 'positions issue closer to the middle if we are at the very bottom' do
+ issue.update relative_position: 6000
+ issue1.update relative_position: nil
+
+ new_issue.move_between(issue, nil)
+
+ expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
+ end
+
+ it 'positions issue in the middle of other two if distance is not big enough' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 400
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to eq(250)
+ end
+
+ it 'positions issue in the middle of other two is there is no place' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 101
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position)
+ end
+
+ it 'uses rebalancing if there is no place' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 101
+ issue2 = create(:issue, relative_position: 102, project: project)
+ new_issue.update relative_position: 103
+
+ new_issue.move_between(issue1, issue2)
+ new_issue.save!
+
+ expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ expect(issue.reload.relative_position).not_to eq(100)
+ end
+
+ it 'positions issue right if we pass none-sequential parameters' do
+ issue.update relative_position: 99
+ issue1.update relative_position: 101
+ issue2 = create(:issue, relative_position: 102, project: project)
+ new_issue.update relative_position: 103
+
+ new_issue.move_between(issue, issue2)
+ new_issue.save!
+
+ expect(new_issue.relative_position).to be(100)
+ end
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 62740ec29fd..aa14709bc9c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,6 +1,7 @@
require 'capybara/rails'
require 'capybara/rspec'
require 'capybara/poltergeist'
+require 'capybara-screenshot/rspec'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
@@ -21,12 +22,8 @@ end
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
-unless ENV['CI'] || ENV['CI_SERVER']
- require 'capybara-screenshot/rspec'
-
- # Keep only the screenshots generated from the last failing test suite
- Capybara::Screenshot.prune_strategy = :keep_last_run
-end
+# Keep only the screenshots generated from the last failing test suite
+Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config|
config.before(:suite) do