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 ...@@ -63,7 +63,7 @@ GEM
fog-core fog-core
mime-types (>= 2.99) mime-types (>= 2.99)
unf unf
ast (2.3.0) ast (2.4.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
...@@ -616,8 +616,8 @@ GEM ...@@ -616,8 +616,8 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.1) parallel (1.12.1)
parser (2.4.0.2) parser (2.5.0.3)
ast (~> 2.3) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.5.0)
blankslate (~> 2.0) blankslate (~> 2.0)
path_expander (1.0.2) path_expander (1.0.2)
...@@ -981,13 +981,13 @@ GEM ...@@ -981,13 +981,13 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
unparser (0.2.6) unparser (0.2.7)
abstract_type (~> 0.0.7) abstract_type (~> 0.0.7)
adamantium (~> 0.2.0) adamantium (~> 0.2.0)
concord (~> 0.1.5) concord (~> 0.1.5)
diff-lcs (~> 1.3) diff-lcs (~> 1.3)
equalizer (~> 0.0.9) equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5) parser (>= 2.3.1.2, < 2.6)
procto (~> 0.0.2) procto (~> 0.0.2)
url_safe_base64 (0.2.2) url_safe_base64 (0.2.2)
validates_hostname (1.0.6) validates_hostname (1.0.6)
......
...@@ -27,10 +27,11 @@ export default { ...@@ -27,10 +27,11 @@ export default {
return Vue.http[method](endpoint); return Vue.http[method](endpoint);
}, },
poll(data = {}) { poll(data = {}) {
const { endpoint, lastFetchedAt } = data; const endpoint = data.notesData.notesPath;
const lastFetchedAt = data.lastFetchedAt;
const options = { const options = {
headers: { headers: {
'X-Last-Fetched-At': lastFetchedAt, 'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined,
}, },
}; };
......
...@@ -198,18 +198,16 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { ...@@ -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; return resp;
}; };
export const poll = ({ commit, state, getters }) => { export const poll = ({ commit, state, getters }) => {
const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
eTagPoll = new Poll({ eTagPoll = new Poll({
resource: service, resource: service,
method: 'poll', method: 'poll',
data: requestData, data: state,
successCallback: resp => resp.json() successCallback: resp => resp.json()
.then(data => pollSuccessCallBack(data, commit, state, getters)), .then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'), errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
...@@ -218,7 +216,7 @@ export const poll = ({ commit, state, getters }) => { ...@@ -218,7 +216,7 @@ export const poll = ({ commit, state, getters }) => {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
eTagPoll.makeRequest(); eTagPoll.makeRequest();
} else { } else {
service.poll(requestData); service.poll(state);
} }
Visibility.change(() => { Visibility.change(() => {
......
...@@ -90,19 +90,21 @@ export default { ...@@ -90,19 +90,21 @@ export default {
const notes = []; const notes = [];
notesData.forEach((note) => { notesData.forEach((note) => {
const nn = Object.assign({}, note);
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) { if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => { note.notes.forEach((n) => {
nn.notes = [n]; // override notes array to only have one item to mimick individual_note notes.push({
notes.push(nn); ...note,
notes: [n], // override notes array to only have one item to mimick individual_note
});
}); });
} else { } else {
const oldNote = utils.findNoteObjectById(state.notes, note.id); 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 ...@@ -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 WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; 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 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 MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.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'; export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
......
...@@ -71,7 +71,8 @@ export default { ...@@ -71,7 +71,8 @@ export default {
return this.mr.deployments.length; return this.mr.deployments.length;
}, },
shouldRenderSourceBranchRemovalStatus() { shouldRenderSourceBranchRemovalStatus() {
return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch; return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch &&
(!this.mr.isNothingToMergeState && !this.mr.isMergedState);
}, },
}, },
methods: { methods: {
......
...@@ -128,6 +128,10 @@ export default class MergeRequestStore { ...@@ -128,6 +128,10 @@ export default class MergeRequestStore {
return this.state === stateKey.nothingToMerge; return this.state === stateKey.nothingToMerge;
} }
get isMergedState() {
return this.state === stateKey.merged;
}
initRebase(data) { initRebase(data) {
this.canPushToSourceBranch = data.can_push_to_source_branch; this.canPushToSourceBranch = data.can_push_to_source_branch;
this.rebaseInProgress = data.rebase_in_progress; this.rebaseInProgress = data.rebase_in_progress;
......
...@@ -49,6 +49,7 @@ export const stateKey = { ...@@ -49,6 +49,7 @@ export const stateKey = {
notAllowedToMerge: 'notAllowedToMerge', notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge', readyToMerge: 'readyToMerge',
rebase: 'rebase', rebase: 'rebase',
merged: 'merged',
}; };
export default { export default {
......
...@@ -137,12 +137,22 @@ ...@@ -137,12 +137,22 @@
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
a:not(.btn-retry), a:not(.btn) {
.btn-link {
color: inherit; color: inherit;
&:hover {
color: $gl-link-hover-color;
.avatar {
border-color: rgba($avatar-border, .2);
}
}
} }
.btn-link { .btn-link {
color: inherit;
outline: none; outline: none;
} }
...@@ -214,7 +224,7 @@ ...@@ -214,7 +224,7 @@
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
color: $md-link-color; color: $gl-link-hover-color;
} }
} }
} }
...@@ -487,16 +497,6 @@ ...@@ -487,16 +497,6 @@
} }
} }
a:not(.btn-retry) {
&:hover {
color: $md-link-color;
.avatar {
border-color: rgba($avatar-border, .2);
}
}
}
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 100%; width: 100%;
padding-top: 6px; padding-top: 6px;
...@@ -504,6 +504,20 @@ ...@@ -504,6 +504,20 @@
.dropdown-menu { .dropdown-menu {
width: 100%; 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 ...@@ -169,7 +169,7 @@ module NotesHelper
reopenPath: reopen_issuable_path(issuable), reopenPath: reopen_issuable_path(issuable),
notesPath: notes_url, notesPath: notes_url,
totalNotes: issuable.discussions.length, totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now lastFetchedAt: Time.now.to_i
}.to_json }.to_json
end end
......
class Compare class Compare
include Gitlab::Utils::StrongMemoize
delegate :same, :head, :base, to: :@compare delegate :same, :head, :base, to: :@compare
attr_reader :project attr_reader :project
...@@ -11,9 +13,10 @@ class Compare ...@@ -11,9 +13,10 @@ class Compare
end end
end end
def initialize(compare, project, straight: false) def initialize(compare, project, base_sha: nil, straight: false)
@compare = compare @compare = compare
@project = project @project = project
@base_sha = base_sha
@straight = straight @straight = straight
end end
...@@ -22,40 +25,36 @@ class Compare ...@@ -22,40 +25,36 @@ class Compare
end end
def start_commit 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 end
def head_commit 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
end
alias_method :commit, :head_commit
def base_commit
return @base_commit if defined?(@base_commit)
@base_commit = if start_commit && head_commit ::Commit.new(commit, project) if commit
project.merge_base_commit(start_commit.id, head_commit.id)
else
nil
end end
end end
alias_method :commit, :head_commit
def start_commit_sha def start_commit_sha
start_commit.try(:sha) start_commit&.sha
end end
def base_commit_sha 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 end
def head_commit_sha def head_commit_sha
commit.try(:sha) commit&.sha
end end
def raw_diffs(*args) def raw_diffs(*args)
......
...@@ -10,9 +10,14 @@ class CompareService ...@@ -10,9 +10,14 @@ class CompareService
@start_ref_name = new_start_ref_name @start_ref_name = new_start_ref_name
end 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) 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
end end
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath", ":root-path" => "rootPath",
":board-id" => "boardId", ":board-id" => "boardId",
":key" => "_uid" } ":key" => "list.id" }
= render "shared/boards/components/sidebar", group: group = render "shared/boards/components/sidebar", group: group
- if @project - if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -2338,6 +2338,8 @@ ActiveRecord::Schema.define(version: 20180309121820) do ...@@ -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", ["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", ["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", ["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 add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
create_table "trending_projects", force: :cascade do |t| create_table "trending_projects", force: :cascade do |t|
......
...@@ -548,6 +548,57 @@ On those a default key should not be provided. ...@@ -548,6 +548,57 @@ On those a default key should not be provided.
1. Properties in a Vue Component: 1. Properties in a Vue Component:
Check [order of properties in components rule][vue-order]. 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 #### Vue and Bootstrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components 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: ...@@ -53,13 +53,13 @@ you can find a clear separation of concerns:
``` ```
new_feature new_feature
├── components ├── components
│ └── component.js.es6 │ └── component.vue
│ └── ... │ └── ...
├── store ├── stores
│ └── new_feature_store.js.es6 │ └── new_feature_store.js
├── service ├── services
│ └── new_feature_service.js.es6 │ └── new_feature_service.js
├── new_feature_bundle.js.es6 ├── new_feature_bundle.js
``` ```
_For consistency purposes, we recommend you to follow the same structure._ _For consistency purposes, we recommend you to follow the same structure._
......
...@@ -15,7 +15,7 @@ module API ...@@ -15,7 +15,7 @@ module API
url_options = Gitlab::Application.routes.default_url_options url_options = Gitlab::Application.routes.default_url_options
protocol, host, port = url_options.slice(:protocol, :host, :port).values 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 end
private private
......
...@@ -44,7 +44,11 @@ module Gitlab ...@@ -44,7 +44,11 @@ module Gitlab
project.commit(head_sha) project.commit(head_sha)
else else
straight = start_sha == base_sha 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 end
end end
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
def initialize(opts = {}) def initialize(opts = {})
@id = opts.fetch(:id, nil) @id = opts.fetch(:id, nil)
@filename = opts.fetch(:filename, nil) @filename = encode_utf8(opts.fetch(:filename, nil))
@basename = opts.fetch(:basename, nil) @basename = encode_utf8(opts.fetch(:basename, nil))
@ref = opts.fetch(:ref, nil) @ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil) @startline = opts.fetch(:startline, nil)
@data = encode_utf8(opts.fetch(:data, nil)) @data = encode_utf8(opts.fetch(:data, nil))
......
/* eslint-disable */ /* eslint-disable */
export const notesDataMock = { export const notesDataMock = {
discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json',
lastFetchedAt: '1501862675', lastFetchedAt: 1501862675,
markdownDocsPath: '/help/user/markdown', markdownDocsPath: '/help/user/markdown',
newSessionPath: '/users/sign_in?redirect_to_referer=yes', newSessionPath: '/users/sign_in?redirect_to_referer=yes',
notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes', notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes',
......
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore'; import _ from 'underscore';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import * as actions from '~/notes/stores/actions'; import * as actions from '~/notes/stores/actions';
import store from '~/notes/stores'; import store from '~/notes/stores';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
...@@ -129,4 +130,68 @@ describe('Actions Notes Store', () => { ...@@ -129,4 +130,68 @@ describe('Actions Notes Store', () => {
], done); ], 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', () => { ...@@ -101,10 +101,21 @@ describe('Notes Store mutations', () => {
const state = { const state = {
notes: [], 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[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 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', () => { describe('template', () => {
const Component = Vue.extend(nothingToMergeComponent); const Component = Vue.extend(NothingToMerge);
const newBlobPath = '/foo'; const newBlobPath = '/foo';
const vm = new Component({ const vm = new Component({
el: document.createElement('div'), el: document.createElement('div'),
......
...@@ -783,15 +783,18 @@ describe('ee merge request widget options', () => { ...@@ -783,15 +783,18 @@ describe('ee merge request widget options', () => {
}); });
describe('rendering source branch removal status', () => { describe('rendering source branch removal status', () => {
it('renders when user cannot remove branch and branch should be removed', (done) => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
mrData: { mrData: {
...mockData, ...mockData,
}, },
}); });
});
it('renders when user cannot remove branch and branch should be removed', (done) => {
vm.mr.canRemoveSourceBranch = false; vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true; vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'readyToMerge';
vm.$nextTick(() => { vm.$nextTick(() => {
const tooltip = vm.$el.querySelector('.fa-question-circle'); const tooltip = vm.$el.querySelector('.fa-question-circle');
...@@ -804,5 +807,18 @@ describe('ee merge request widget options', () => { ...@@ -804,5 +807,18 @@ describe('ee merge request widget options', () => {
done(); 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', () => { ...@@ -82,6 +82,10 @@ describe('mrWidgetOptions', () => {
}); });
describe('shouldRenderSourceBranchRemovalStatus', () => { describe('shouldRenderSourceBranchRemovalStatus', () => {
beforeEach(() => {
vm.mr.state = 'readyToMerge';
});
it('should return true when cannot remove source branch and branch will be removed', () => { it('should return true when cannot remove source branch and branch will be removed', () => {
vm.mr.canRemoveSourceBranch = false; vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true; vm.mr.shouldRemoveSourceBranch = true;
...@@ -102,6 +106,22 @@ describe('mrWidgetOptions', () => { ...@@ -102,6 +106,22 @@ describe('mrWidgetOptions', () => {
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false); 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', () => { describe('shouldRenderDeployments', () => {
...@@ -407,6 +427,7 @@ describe('mrWidgetOptions', () => { ...@@ -407,6 +427,7 @@ describe('mrWidgetOptions', () => {
it('renders when user cannot remove branch and branch should be removed', (done) => { it('renders when user cannot remove branch and branch should be removed', (done) => {
vm.mr.canRemoveSourceBranch = false; vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true; vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'readyToMerge';
vm.$nextTick(() => { vm.$nextTick(() => {
const tooltip = vm.$el.querySelector('.fa-question-circle'); const tooltip = vm.$el.querySelector('.fa-question-circle');
...@@ -419,5 +440,18 @@ describe('mrWidgetOptions', () => { ...@@ -419,5 +440,18 @@ describe('mrWidgetOptions', () => {
done(); 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 ...@@ -108,14 +108,26 @@ describe Gitlab::ProjectSearchResults do
context 'when the search returns non-ASCII data' do context 'when the search returns non-ASCII data' do
context 'with UTF-8' 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 it 'returns results as UTF-8' do
expect(subject.filename).to eq('encoding/russian.rb') expect(subject.filename).to eq('encoding/russian.rb')
expect(subject.basename).to eq('encoding/russian') expect(subject.basename).to eq('encoding/russian')
expect(subject.ref).to eq('master') expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1) 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
end end
......
...@@ -37,33 +37,51 @@ describe Compare do ...@@ -37,33 +37,51 @@ describe Compare do
end end
end end
describe '#base_commit' do describe '#base_commit_sha' do
let(:base_commit) { Commit.new(another_sample_commit, project) } it 'returns @base_sha if it is present' do
expect(project).not_to receive(:merge_base_commit)
it 'returns project merge base commit' do sha = double
expect(project).to receive(:merge_base_commit).with(start_commit.id, head_commit.id).and_return(base_commit) 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 end
it 'returns nil if there is no start_commit' do it 'returns nil if there is no start_commit' do
expect(subject).to receive(:start_commit).and_return(nil) 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 end
it 'returns nil if there is no head commit' do it 'returns nil if there is no head commit' do
expect(subject).to receive(:head_commit).and_return(nil) 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
end end
describe '#diff_refs' do describe '#diff_refs' do
it 'uses base_commit sha as base_sha' do it 'uses base_commit_sha 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_sha)
expect(subject.diff_refs.base_sha).to eq(subject.base_commit.id)
end end
it 'uses start_commit sha as start_sha' do 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