Commit be5cb4e9 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-03-12' into 'master'

CE upstream - 2018-03-12 18:25 UTC

Closes #5204 and #5217

See merge request gitlab-org/gitlab-ee!4939
parents 7985ca9c 61625d8b
......@@ -63,7 +63,7 @@ GEM
fog-core
mime-types (>= 2.99)
unf
ast (2.3.0)
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
......@@ -616,8 +616,8 @@ GEM
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.1)
parser (2.4.0.2)
ast (~> 2.3)
parser (2.5.0.3)
ast (~> 2.4.0)
parslet (1.5.0)
blankslate (~> 2.0)
path_expander (1.0.2)
......@@ -981,13 +981,13 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
unparser (0.2.6)
unparser (0.2.7)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5)
parser (>= 2.3.1.2, < 2.6)
procto (~> 0.0.2)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
......
......@@ -27,10 +27,11 @@ export default {
return Vue.http[method](endpoint);
},
poll(data = {}) {
const { endpoint, lastFetchedAt } = data;
const endpoint = data.notesData.notesPath;
const lastFetchedAt = data.lastFetchedAt;
const options = {
headers: {
'X-Last-Fetched-At': lastFetchedAt,
'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined,
},
};
......
......@@ -198,18 +198,16 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
});
}
commit(types.SET_LAST_FETCHED_AT, resp.lastFetchedAt);
commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at);
return resp;
};
export const poll = ({ commit, state, getters }) => {
const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
eTagPoll = new Poll({
resource: service,
method: 'poll',
data: requestData,
data: state,
successCallback: resp => resp.json()
.then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
......@@ -218,7 +216,7 @@ export const poll = ({ commit, state, getters }) => {
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
service.poll(requestData);
service.poll(state);
}
Visibility.change(() => {
......
......@@ -90,19 +90,21 @@ export default {
const notes = [];
notesData.forEach((note) => {
const nn = Object.assign({}, note);
// To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => {
nn.notes = [n]; // override notes array to only have one item to mimick individual_note
notes.push(nn);
notes.push({
...note,
notes: [n], // override notes array to only have one item to mimick individual_note
});
});
} else {
const oldNote = utils.findNoteObjectById(state.notes, note.id);
nn.expanded = oldNote ? oldNote.expanded : note.expanded;
notes.push(nn);
notes.push({
...note,
expanded: (oldNote ? oldNote.expanded : note.expanded),
});
}
});
......
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default {
name: 'MRWidgetNothingToMerge',
props: {
mr: {
type: Object,
required: true,
},
},
data() {
return { emptyStateSVG };
},
template: `
<div class="mr-widget-body mr-widget-empty-state">
<div class="row">
<div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
<span v-html="emptyStateSVG"></span>
</div>
<div class="text col-sm-7 col-sm-pull-5 col-xs-12">
<span>
Merge requests are a place to propose changes you have made to a project
and discuss those changes with others.
</span>
<p>
Interested parties can even contribute by pushing commits if they want to.
</p>
<p>
Currently there are no changes in this merge request's source branch.
Please push new commits or use a different branch.
</p>
<div>
<a
v-if="mr.newBlobPath"
:href="mr.newBlobPath"
class="btn btn-inverted btn-save">
Create file
</a>
</div>
</div>
</div>
</div>
`,
};
<script>
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default {
name: 'MRWidgetNothingToMerge',
props: {
mr: {
type: Object,
required: true,
},
},
data() {
return { emptyStateSVG };
},
};
</script>
<template>
<div class="mr-widget-body mr-widget-empty-state">
<div class="row">
<div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
<span v-html="emptyStateSVG"></span>
</div>
<div class="text col-sm-7 col-sm-pull-5 col-xs-12">
<span>
Merge requests are a place to propose changes you have made to a project
and discuss those changes with others.
</span>
<p>
Interested parties can even contribute by pushing commits if they want to.
</p>
<p>
Currently there are no changes in this merge request's source branch.
Please push new commits or use a different branch.
</p>
<div>
<a
v-if="mr.newBlobPath"
:href="mr.newBlobPath"
class="btn btn-inverted btn-save">
Create file
</a>
</div>
</div>
</div>
</div>
</template>
......@@ -24,7 +24,7 @@ export { default as MergingState } from './components/states/mr_widget_merging.v
export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
......
......@@ -71,7 +71,8 @@ export default {
return this.mr.deployments.length;
},
shouldRenderSourceBranchRemovalStatus() {
return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch;
return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch &&
(!this.mr.isNothingToMergeState && !this.mr.isMergedState);
},
},
methods: {
......
......@@ -128,6 +128,10 @@ export default class MergeRequestStore {
return this.state === stateKey.nothingToMerge;
}
get isMergedState() {
return this.state === stateKey.merged;
}
initRebase(data) {
this.canPushToSourceBranch = data.can_push_to_source_branch;
this.rebaseInProgress = data.rebase_in_progress;
......
......@@ -49,6 +49,7 @@ export const stateKey = {
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
rebase: 'rebase',
merged: 'merged',
};
export default {
......
......@@ -137,12 +137,22 @@
z-index: 200;
overflow: hidden;
a:not(.btn-retry),
.btn-link {
a:not(.btn) {
color: inherit;
&:hover {
color: $gl-link-hover-color;
.avatar {
border-color: rgba($avatar-border, .2);
}
}
}
.btn-link {
color: inherit;
outline: none;
}
......@@ -214,7 +224,7 @@
&:hover {
text-decoration: underline;
color: $md-link-color;
color: $gl-link-hover-color;
}
}
}
......@@ -487,16 +497,6 @@
}
}
a:not(.btn-retry) {
&:hover {
color: $md-link-color;
.avatar {
border-color: rgba($avatar-border, .2);
}
}
}
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
......@@ -504,6 +504,20 @@
.dropdown-menu {
width: 100%;
/*
* Overwrite hover style for dropdown items, so that they are not blue
* This should be removed during dev of https://gitlab.com/gitlab-org/gitlab-ce/issues/44040
*/
li a {
&:hover,
&:active,
&:focus,
&.is-focused {
@include dropdown-item-hover;
}
}
}
}
......
......@@ -169,7 +169,7 @@ module NotesHelper
reopenPath: reopen_issuable_path(issuable),
notesPath: notes_url,
totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now
lastFetchedAt: Time.now.to_i
}.to_json
end
......
class Compare
include Gitlab::Utils::StrongMemoize
delegate :same, :head, :base, to: :@compare
attr_reader :project
......@@ -11,9 +13,10 @@ class Compare
end
end
def initialize(compare, project, straight: false)
def initialize(compare, project, base_sha: nil, straight: false)
@compare = compare
@project = project
@base_sha = base_sha
@straight = straight
end
......@@ -22,40 +25,36 @@ class Compare
end
def start_commit
return @start_commit if defined?(@start_commit)
strong_memoize(:start_commit) do
commit = @compare.base
commit = @compare.base
@start_commit = commit ? ::Commit.new(commit, project) : nil
::Commit.new(commit, project) if commit
end
end
def head_commit
return @head_commit if defined?(@head_commit)
strong_memoize(:head_commit) do
commit = @compare.head
commit = @compare.head
@head_commit = commit ? ::Commit.new(commit, project) : nil
::Commit.new(commit, project) if commit
end
end
alias_method :commit, :head_commit
def base_commit
return @base_commit if defined?(@base_commit)
@base_commit = if start_commit && head_commit
project.merge_base_commit(start_commit.id, head_commit.id)
else
nil
end
end
def start_commit_sha
start_commit.try(:sha)
start_commit&.sha
end
def base_commit_sha
base_commit.try(:sha)
strong_memoize(:base_commit) do
next unless start_commit && head_commit
@base_sha || project.merge_base_commit(start_commit.id, head_commit.id)&.sha
end
end
def head_commit_sha
commit.try(:sha)
commit&.sha
end
def raw_diffs(*args)
......
......@@ -10,9 +10,14 @@ class CompareService
@start_ref_name = new_start_ref_name
end
def execute(target_project, target_ref, straight: false)
def execute(target_project, target_ref, base_sha: nil, straight: false)
raw_compare = target_project.repository.compare_source_branch(target_ref, start_project.repository, start_ref_name, straight: straight)
Compare.new(raw_compare, target_project, straight: straight) if raw_compare
return unless raw_compare
Compare.new(raw_compare,
target_project,
base_sha: base_sha,
straight: straight)
end
end
......@@ -28,7 +28,7 @@
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":board-id" => "boardId",
":key" => "_uid" }
":key" => "list.id" }
= render "shared/boards/components/sidebar", group: group
- if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
......
---
title: Fix hover style of dropdown items in the right sidebar
merge_request: 17519
author:
type: fixed
---
title: Use object ID to prevent duplicate keys Vue warning on Issue Boards page during
development
merge_request: 17682
author:
type: other
---
title: Add partial indexes on todos to handle users with many todos
merge_request:
author:
type: performance
---
title: Fix code and wiki search results when filename is non-ASCII
merge_request:
author:
type: fixed
---
title: Avoid re-fetching merge-base SHA from Gitaly unnecessarily
merge_request:
author:
type: performance
---
title: Move NothingToMerge vue component
merge_request: 17544
author: George Tsiolis
type: performance
---
title: Ensure the API returns https links when https is configured
merge_request: 17681
author:
type: fixed
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPartialIndexesOnTodos < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME_PENDING="index_todos_on_user_id_and_id_pending"
INDEX_NAME_DONE="index_todos_on_user_id_and_id_done"
def up
unless index_exists?(:todos, [:user_id, :id], name: INDEX_NAME_PENDING)
add_concurrent_index(:todos, [:user_id, :id], where: "state='pending'", name: INDEX_NAME_PENDING)
end
unless index_exists?(:todos, [:user_id, :id], name: INDEX_NAME_DONE)
add_concurrent_index(:todos, [:user_id, :id], where: "state='done'", name: INDEX_NAME_DONE)
end
end
def down
remove_concurrent_index(:todos, [:user_id, :id], where: "state='pending'", name: INDEX_NAME_PENDING)
remove_concurrent_index(:todos, [:user_id, :id], where: "state='done'", name: INDEX_NAME_DONE)
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180309121820) do
ActiveRecord::Schema.define(version: 20180309160427) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -2338,6 +2338,8 @@ ActiveRecord::Schema.define(version: 20180309121820) do
add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
add_index "todos", ["user_id", "id"], name: "index_todos_on_user_id_and_id_done", where: "((state)::text = 'done'::text)", using: :btree
add_index "todos", ["user_id", "id"], name: "index_todos_on_user_id_and_id_pending", where: "((state)::text = 'pending'::text)", using: :btree
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
create_table "trending_projects", force: :cascade do |t|
......
......@@ -548,6 +548,57 @@ On those a default key should not be provided.
1. Properties in a Vue Component:
Check [order of properties in components rule][vue-order].
#### `:key`
When using `v-for` you need to provide a *unique* `:key` attribute for each item.
1. If the elements of the array being iterated have an unique `id` it is advised to use it:
```html
<div
v-for="item in items"
:key="item.id"
>
<!-- content -->
</div>
```
1. When the elements being iterated don't have a unique id, you can use the array index as the `:key` attribute
```html
<div
v-for="(item, index) in items"
:key="index"
>
<!-- content -->
</div>
```
1. When using `v-for` with `template` and there is more than one child element, the `:key` values must be unique. It's advised to use `kebab-case` namespaces.
```html
<template v-for="(item, index) in items">
<span :key="`span-${index}`"></span>
<button :key="`button-${index}`"></button>
</template>
```
1. When dealing with nested `v-for` use the same guidelines as above.
```html
<div
v-for="item in items"
:key="item.id"
>
<span
v-for="element in array"
:key="element.id"
>
<!-- content -->
</span>
</div>
```
Useful links:
1. [`key`](https://vuejs.org/v2/guide/list.html#key)
1. [Vue Style Guide: Keyed v-for](https://vuejs.org/v2/style-guide/#Keyed-v-for-essential )
#### Vue and Bootstrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
......
......@@ -53,13 +53,13 @@ you can find a clear separation of concerns:
```
new_feature
├── components
│ └── component.js.es6
│ └── component.vue
│ └── ...
├── store
│ └── new_feature_store.js.es6
├── service
│ └── new_feature_service.js.es6
├── new_feature_bundle.js.es6
├── stores
│ └── new_feature_store.js
├── services
│ └── new_feature_service.js
├── new_feature_bundle.js
```
_For consistency purposes, we recommend you to follow the same structure._
......
......@@ -15,7 +15,7 @@ module API
url_options = Gitlab::Application.routes.default_url_options
protocol, host, port = url_options.slice(:protocol, :host, :port).values
URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s
URI::Generic.build(scheme: protocol, host: host, port: port, path: path).to_s
end
private
......
......@@ -44,7 +44,11 @@ module Gitlab
project.commit(head_sha)
else
straight = start_sha == base_sha
CompareService.new(project, head_sha).execute(project, start_sha, straight: straight)
CompareService.new(project, head_sha).execute(project,
start_sha,
base_sha: base_sha,
straight: straight)
end
end
end
......
......@@ -7,8 +7,8 @@ module Gitlab
def initialize(opts = {})
@id = opts.fetch(:id, nil)
@filename = opts.fetch(:filename, nil)
@basename = opts.fetch(:basename, nil)
@filename = encode_utf8(opts.fetch(:filename, nil))
@basename = encode_utf8(opts.fetch(:basename, nil))
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
@data = encode_utf8(opts.fetch(:data, nil))
......
/* eslint-disable */
export const notesDataMock = {
discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json',
lastFetchedAt: '1501862675',
lastFetchedAt: 1501862675,
markdownDocsPath: '/help/user/markdown',
newSessionPath: '/users/sign_in?redirect_to_referer=yes',
notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes',
......
import Vue from 'vue';
import _ from 'underscore';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import * as actions from '~/notes/stores/actions';
import store from '~/notes/stores';
import testAction from '../../helpers/vuex_action_helper';
......@@ -129,4 +130,68 @@ describe('Actions Notes Store', () => {
], done);
});
});
describe('poll', () => {
beforeEach((done) => {
jasmine.clock().install();
spyOn(Vue.http, 'get').and.callThrough();
store.dispatch('setNotesData', notesDataMock)
.then(done)
.catch(done.fail);
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('calls service with last fetched state', (done) => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
notes: [],
last_fetched_at: '123456',
}), {
status: 200,
headers: {
'poll-interval': '1000',
},
}));
};
Vue.http.interceptors.push(interceptor);
Vue.http.interceptors.push(headersInterceptor);
store.dispatch('poll')
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
.then(() => {
expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), {
url: jasmine.anything(),
method: 'get',
headers: {
'X-Last-Fetched-At': undefined,
},
});
expect(store.state.lastFetchedAt).toBe('123456');
jasmine.clock().tick(1500);
})
.then(() => new Promise((resolve) => {
requestAnimationFrame(resolve);
}))
.then(() => {
expect(Vue.http.get.calls.count()).toBe(2);
expect(Vue.http.get.calls.mostRecent().args[1].headers).toEqual({
'X-Last-Fetched-At': '123456',
});
})
.then(() => store.dispatch('stopPolling'))
.then(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -101,10 +101,21 @@ describe('Notes Store mutations', () => {
const state = {
notes: [],
};
const legacyNote = {
id: 2,
individual_note: true,
notes: [{
note: '1',
}, {
note: '2',
}],
};
mutations.SET_INITIAL_NOTES(state, [note]);
mutations.SET_INITIAL_NOTES(state, [note, legacyNote]);
expect(state.notes[0].id).toEqual(note.id);
expect(state.notes.length).toEqual(1);
expect(state.notes[1].notes[0].note).toBe(legacyNote.notes[0].note);
expect(state.notes[2].notes[0].note).toBe(legacyNote.notes[1].note);
expect(state.notes.length).toEqual(3);
});
});
......
import Vue from 'vue';
import nothingToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge';
import NothingToMerge from '~/vue_merge_request_widget/components/states/nothing_to_merge.vue';
describe('MRWidgetNothingToMerge', () => {
describe('NothingToMerge', () => {
describe('template', () => {
const Component = Vue.extend(nothingToMergeComponent);
const Component = Vue.extend(NothingToMerge);
const newBlobPath = '/foo';
const vm = new Component({
el: document.createElement('div'),
......
......@@ -783,15 +783,18 @@ describe('ee merge request widget options', () => {
});
describe('rendering source branch removal status', () => {
it('renders when user cannot remove branch and branch should be removed', (done) => {
beforeEach(() => {
vm = mountComponent(Component, {
mrData: {
...mockData,
},
});
});
it('renders when user cannot remove branch and branch should be removed', (done) => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'readyToMerge';
vm.$nextTick(() => {
const tooltip = vm.$el.querySelector('.fa-question-circle');
......@@ -804,5 +807,18 @@ describe('ee merge request widget options', () => {
done();
});
});
it('does not render in merged state', (done) => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'merged';
vm.$nextTick(() => {
expect(vm.$el.textContent).toContain('The source branch has been removed');
expect(vm.$el.textContent).not.toContain('Removes source branch');
done();
});
});
});
});
......@@ -82,6 +82,10 @@ describe('mrWidgetOptions', () => {
});
describe('shouldRenderSourceBranchRemovalStatus', () => {
beforeEach(() => {
vm.mr.state = 'readyToMerge';
});
it('should return true when cannot remove source branch and branch will be removed', () => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
......@@ -102,6 +106,22 @@ describe('mrWidgetOptions', () => {
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
});
it('should return false when in merged state', () => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'merged';
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
});
it('should return false when in nothing to merge state', () => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'nothingToMerge';
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
});
});
describe('shouldRenderDeployments', () => {
......@@ -407,6 +427,7 @@ describe('mrWidgetOptions', () => {
it('renders when user cannot remove branch and branch should be removed', (done) => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'readyToMerge';
vm.$nextTick(() => {
const tooltip = vm.$el.querySelector('.fa-question-circle');
......@@ -419,5 +440,18 @@ describe('mrWidgetOptions', () => {
done();
});
});
it('does not render in merged state', (done) => {
vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'merged';
vm.$nextTick(() => {
expect(vm.$el.textContent).toContain('The source branch has been removed');
expect(vm.$el.textContent).not.toContain('Removes source branch');
done();
});
});
});
});
require 'spec_helper'
describe API::Helpers::RelatedResourcesHelpers do
subject(:helpers) do
Class.new.include(described_class).new
end
describe '#expose_url' do
let(:path) { '/api/v4/awesome_endpoint' }
subject(:url) { helpers.expose_url(path) }
def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil)
expect(Gitlab::Application.routes).to receive(:default_url_options)
.and_return(protocol: protocol, host: host, port: port)
end
it 'respects the protocol if it is HTTP' do
stub_default_url_options(protocol: 'http')
is_expected.to start_with('http://')
end
it 'respects the protocol if it is HTTPS' do
stub_default_url_options(protocol: 'https')
is_expected.to start_with('https://')
end
it 'accepts port to be nil' do
stub_default_url_options(port: nil)
is_expected.to start_with('http://example.com/')
end
it 'includes port if provided' do
stub_default_url_options(port: 8080)
is_expected.to start_with('http://example.com:8080/')
end
end
end
......@@ -108,14 +108,26 @@ describe Gitlab::ProjectSearchResults do
context 'when the search returns non-ASCII data' do
context 'with UTF-8' do
let(:results) { project.repository.search_files_by_content("файл", 'master') }
let(:results) { project.repository.search_files_by_content('файл', 'master') }
it 'returns results as UTF-8' do
expect(subject.filename).to eq('encoding/russian.rb')
expect(subject.basename).to eq('encoding/russian')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
expect(subject.data).to eq("Хороший файл")
expect(subject.data).to eq('Хороший файл')
end
end
context 'with UTF-8 in the filename' do
let(:results) { project.repository.search_files_by_content('webhook', 'master') }
it 'returns results as UTF-8' do
expect(subject.filename).to eq('encoding/テスト.txt')
expect(subject.basename).to eq('encoding/テスト')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(3)
expect(subject.data).to include('WebHookの確認')
end
end
......
......@@ -37,33 +37,51 @@ describe Compare do
end
end
describe '#base_commit' do
let(:base_commit) { Commit.new(another_sample_commit, project) }
describe '#base_commit_sha' do
it 'returns @base_sha if it is present' do
expect(project).not_to receive(:merge_base_commit)
it 'returns project merge base commit' do
expect(project).to receive(:merge_base_commit).with(start_commit.id, head_commit.id).and_return(base_commit)
sha = double
service = described_class.new(raw_compare, project, base_sha: sha)
expect(subject.base_commit).to eq(base_commit)
expect(service.base_commit_sha).to eq(sha)
end
it 'fetches merge base SHA from repo when @base_sha is nil' do
expect(project).to receive(:merge_base_commit)
.with(start_commit.id, head_commit.id)
.once
.and_call_original
expect(subject.base_commit_sha)
.to eq(project.repository.merge_base(start_commit.id, head_commit.id))
end
it 'is memoized on first call' do
expect(project).to receive(:merge_base_commit)
.with(start_commit.id, head_commit.id)
.once
.and_call_original
3.times { subject.base_commit_sha }
end
it 'returns nil if there is no start_commit' do
expect(subject).to receive(:start_commit).and_return(nil)
expect(subject.base_commit).to eq(nil)
expect(subject.base_commit_sha).to eq(nil)
end
it 'returns nil if there is no head commit' do
expect(subject).to receive(:head_commit).and_return(nil)
expect(subject.base_commit).to eq(nil)
expect(subject.base_commit_sha).to eq(nil)
end
end
describe '#diff_refs' do
it 'uses base_commit sha as base_sha' do
expect(subject).to receive(:base_commit).at_least(:once).and_call_original
expect(subject.diff_refs.base_sha).to eq(subject.base_commit.id)
it 'uses base_commit_sha sha as base_sha' do
expect(subject.diff_refs.base_sha).to eq(subject.base_commit_sha)
end
it 'uses start_commit sha as start_sha' do
......
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