Commit e2584841 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-09-17

# Conflicts:
#	app/models/ci/runner.rb
#	app/models/project_authorization.rb
#	app/models/user.rb

[ci skip]
parents 3f986175 1d925e5c
......@@ -70,7 +70,7 @@ to contribute to GitLab in a way that is easy for everyone.
For a first-time step-by-step guide to the contribution process, please see
["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute).
Looking for something to work on? Look for issues in the [Backlog (Accepting merge requests) milestone](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial
......@@ -151,8 +151,8 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
If you want to contribute to GitLab [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight]
is a great place to start. Issues with a lower weight (1 or 2) are deemed
If you want to contribute to GitLab, [issues in the `Backlog (Accepting merge requests)` milestone with small weight][https://gitlab.com/gitlab-org/gitlab-ce/issues?scope=all&utf8=✓&state=opened&assignee_id=0&milestone_title=Backlog%20(Accepting%20merge%20requests)]
are a great place to start. Issues with a lower weight (1 or 2) are deemed
suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
......
<script>
import { mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import ChangedFileIcon from './changed_file_icon.vue';
import MrFileIcon from './mr_file_icon.vue';
export default {
name: 'FileRowExtra',
directives: {
tooltip,
},
components: {
Icon,
NewDropdown,
ChangedFileIcon,
MrFileIcon,
},
props: {
file: {
type: Object,
required: true,
},
mouseOver: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters([
'getChangesInFolder',
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
folderStagedCount() {
return this.getStagedFilesCountForPath(this.file.path);
},
changesCount() {
return this.getChangesInFolder(this.file.path);
},
folderChangesTooltip() {
if (this.changesCount === 0) return undefined;
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
});
},
showTreeChangesCount() {
return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
},
},
};
</script>
<template>
<div class="float-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
<span
v-if="showTreeChangesCount"
class="ide-tree-changes"
>
{{ changesCount }}
<icon
v-tooltip
:title="folderChangesTooltip"
:size="12"
data-container="body"
data-placement="right"
name="file-modified"
css-classes="prepend-left-5 ide-file-modified"
/>
</span>
<changed-file-icon
v-else-if="showChangedFileIcon"
:file="file"
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
/>
<new-dropdown
:type="file.type"
:path="file.path"
:mouse-over="mouseOver"
class="prepend-left-8"
/>
</div>
</template>
......@@ -2,15 +2,16 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
export default {
components: {
Icon,
RepoFile,
SkeletonLoadingContainer,
NavDropdown,
FileRow,
},
props: {
viewerType: {
......@@ -34,8 +35,9 @@ export default {
this.updateViewer(this.viewerType);
},
methods: {
...mapActions(['updateViewer']),
...mapActions(['updateViewer', 'toggleTreeOpen']),
},
FileRowExtra,
};
</script>
......@@ -63,11 +65,13 @@ export default {
<div
class="ide-tree-body h-100"
>
<repo-file
<file-row
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:extra-component="$options.FileRowExtra"
@toggleTreeOpen="toggleTreeOpen"
/>
</div>
</template>
......
<script>
import { mapActions, mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
import NewDropdown from './new_dropdown/index.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
import ChangedFileIcon from './changed_file_icon.vue';
import MrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
directives: {
tooltip,
},
name: 'FileRow',
components: {
SkeletonLoadingContainer,
NewDropdown,
FileStatusIcon,
FileIcon,
ChangedFileIcon,
MrFileIcon,
Icon,
},
props: {
......@@ -34,6 +17,11 @@ export default {
type: Number,
required: true,
},
extraComponent: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
......@@ -41,34 +29,6 @@ export default {
};
},
computed: {
...mapGetters([
'getChangesInFolder',
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
folderStagedCount() {
return this.getStagedFilesCountForPath(this.file.path);
},
changesCount() {
return this.getChangesInFolder(this.file.path);
},
folderChangesTooltip() {
if (this.changesCount === 0) return undefined;
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
});
},
isTree() {
return this.file.type === 'tree';
},
......@@ -83,17 +43,11 @@ export default {
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
'file-active': this.isBlob && this.file.active,
'is-active': this.isBlob && this.file.active,
folder: this.isTree,
'is-open': this.file.opened,
};
},
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
},
},
watch: {
'file.active': function fileActiveWatch(active) {
......@@ -108,14 +62,16 @@ export default {
}
},
methods: {
...mapActions(['toggleTreeOpen']),
toggleTreeOpen(path) {
this.$emit('toggleTreeOpen', path);
},
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.hasUrlAtCurrentRoute()) {
this.toggleTreeOpen(this.file.path);
}
router.push(`/project${this.file.url}`);
if (this.$router) this.$router.push(`/project${this.file.url}`);
},
scrollIntoView(isInit = false) {
const block = isInit && this.isTree ? 'center' : 'nearest';
......@@ -141,6 +97,8 @@ export default {
return filePath === routePath;
},
hasUrlAtCurrentRoute() {
if (!this.$router || !this.$router.currentRoute) return true;
return this.$router.currentRoute.path === `/project${this.file.url}`;
},
toggleHover(over) {
......@@ -154,18 +112,18 @@ export default {
<div>
<div
:class="fileClass"
class="file"
class="file-row"
role="button"
@click="clickFile"
@mouseover="toggleHover(true)"
@mouseout="toggleHover(false)"
>
<div
class="file-name"
class="file-row-name-container"
>
<span
:style="levelIndentation"
class="ide-file-name str-truncated"
class="file-row-name str-truncated"
>
<file-icon
:file-name="file.name"
......@@ -175,53 +133,78 @@ export default {
:size="16"
/>
{{ file.name }}
<file-status-icon
:file="file"
/>
</span>
<span class="float-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
<span
v-if="showTreeChangesCount"
class="ide-tree-changes"
>
{{ changesCount }}
<icon
v-tooltip
:title="folderChangesTooltip"
:size="12"
data-container="body"
data-placement="right"
name="file-modified"
css-classes="prepend-left-5 ide-file-modified"
/>
</span>
<changed-file-icon
v-else-if="showChangedFileIcon"
<component
v-if="extraComponent"
:is="extraComponent"
:file="file"
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
class="float-right"
/>
</span>
<new-dropdown
:type="file.type"
:path="file.path"
:mouse-over="mouseOver"
class="float-right prepend-left-8"
/>
</div>
</div>
<template v-if="file.opened">
<repo-file
<file-row
v-for="childFile in file.tree"
:key="childFile.key"
:file="childFile"
:level="level + 1"
:extra-component="extraComponent"
@toggleTreeOpen="toggleTreeOpen"
/>
</template>
</div>
</template>
<style>
.file-row {
display: flex;
align-items: center;
height: 32px;
padding: 4px 8px;
margin-left: -8px;
margin-right: -8px;
border-radius: 3px;
text-align: left;
cursor: pointer;
}
.file-row:hover,
.file-row:focus {
background: #f2f2f2;
}
.file-row:active {
background: #dfdfdf;
}
.file-row.is-active {
background: #f2f2f2;
}
.file-row-name-container {
display: flex;
width: 100%;
align-items: center;
overflow: visible;
}
.file-row-name {
display: inline-block;
flex: 1;
max-width: inherit;
height: 18px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-row-name svg {
margin-right: 2px;
vertical-align: middle;
}
.file-row-name .loading-container {
display: inline-block;
margin-right: 4px;
}
</style>
......@@ -53,83 +53,9 @@ $ide-commit-header-height: 48px;
flex: 1;
min-height: 0; // firefox fix
.file {
height: 32px;
cursor: pointer;
&.file-active {
background: $theme-gray-100;
}
.ide-file-name {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
line-height: 16px;
display: inline-block;
height: 18px;
svg {
vertical-align: middle;
margin-right: 2px;
}
.loading-container {
margin-right: 4px;
display: inline-block;
}
}
.ide-file-icon-holder {
display: flex;
align-items: center;
color: $theme-gray-700;
}
.ide-file-changed-icon {
margin-left: auto;
> svg {
display: block;
}
}
.ide-new-btn {
display: none;
.btn {
padding: 2px 5px;
}
}
&:hover,
&:focus {
.ide-new-btn {
display: block;
}
}
.folder-icon {
fill: $gl-text-color-secondary;
}
}
a {
color: $gl-text-color;
}
th {
position: sticky;
top: 0;
}
}
.file-name {
display: flex;
overflow: visible;
align-items: center;
width: 100%;
}
.multi-file-loading-container {
......@@ -625,8 +551,7 @@ $ide-commit-header-height: 48px;
}
}
.multi-file-commit-list-path,
.ide-file-list .file {
.multi-file-commit-list-path {
display: flex;
align-items: center;
margin-left: -$grid-size;
......@@ -634,28 +559,14 @@ $ide-commit-header-height: 48px;
padding: $grid-size / 2 $grid-size;
border-radius: $border-radius-default;
text-align: left;
&:hover,
&:focus {
background: $theme-gray-100;
}
&:active {
background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
cursor: pointer;
height: $ide-commit-row-height;
padding-right: 0;
&.is-active {
background-color: $white-normal;
}
&:hover,
&:focus {
background: $theme-gray-100;
outline: 0;
.multi-file-discard-btn {
......@@ -665,6 +576,14 @@ $ide-commit-header-height: 48px;
}
}
&:active {
background: $theme-gray-200;
}
&.is-active {
background-color: $white-normal;
}
svg {
min-width: 16px;
vertical-align: middle;
......@@ -1398,9 +1317,17 @@ $ide-commit-header-height: 48px;
}
}
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
.ide-new-btn {
display: none;
.btn {
padding: 2px 5px;
}
.dropdown.show .ide-entry-dropdown-toggle {
color: $white-normal;
background-color: $blue-500;
}
}
.ide-preview-header {
......@@ -1465,3 +1392,28 @@ $ide-commit-header-height: 48px;
width: $ide-commit-row-height;
height: $ide-commit-row-height;
}
.ide-file-icon-holder {
display: flex;
align-items: center;
color: $theme-gray-700;
}
.ide-file-changed-icon {
margin-left: auto;
> svg {
display: block;
}
}
.file-row:hover,
.file-row:focus {
.ide-new-btn {
display: block;
}
.folder-icon {
fill: $gl-text-color-secondary;
}
}
......@@ -279,6 +279,10 @@ table.u2f-registrations {
}
}
.codes {
padding-top: 14px;
}
.oauth-application-show {
.scope-name {
font-weight: $gl-font-weight-bold;
......
......@@ -8,7 +8,10 @@ module Ci
include RedisCacheable
include ChronicDurationAttribute
include FromUnion
<<<<<<< HEAD
prepend EE::Ci::Runner
=======
>>>>>>> upstream/master
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
......
......@@ -2,7 +2,10 @@
class ProjectAuthorization < ActiveRecord::Base
include FromUnion
<<<<<<< HEAD
prepend ::EE::ProjectAuthorization
=======
>>>>>>> upstream/master
belongs_to :user
belongs_to :project
......
......@@ -21,8 +21,11 @@ class User < ActiveRecord::Base
include WithUploads
include OptionallySearch
include FromUnion
<<<<<<< HEAD
prepend EE::User
=======
>>>>>>> upstream/master
DEFAULT_NOTIFICATION_LEVEL = :participating
......@@ -364,10 +367,13 @@ class User < ActiveRecord::Base
emails = emails.confirmed if confirmed
from_union([users, emails])
<<<<<<< HEAD
end
def existing_member?(email)
User.where(email: email).any? || Email.where(email: email).any?
=======
>>>>>>> upstream/master
end
def filter(filter_name)
......
......@@ -10,4 +10,6 @@
%li
%span.monospace= code
= link_to 'Proceed', profile_account_path, class: 'btn btn-success'
.d-flex
= link_to 'Proceed', profile_account_path, class: 'btn btn-success append-right-10'
= link_to 'Download codes', "data:text/plain;charset=utf-8,#{URI.encode(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'btn btn-default'
---
title: Add button to download 2FA codes
merge_request:
author: Luke Picciau
type: added
---
title: Add Gitaly diff stats RPC client
merge_request: 21732
author:
type: changed
......@@ -1231,13 +1231,16 @@ rspec:
```
The collected JUnit reports will be uploaded to GitLab as an artifact and will
be automatically [shown in merge requests](../junit_test_reports.md).
be automatically shown in merge requests.
For more examples, see [JUnit test reports](../junit_test_reports.md).
NOTE: **Note:**
In case the JUnit tool you use exports to multiple XML files, you can specify
multiple test report paths within a single job
(`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`) and they will be automatically
concatenated into a single file.
multiple test report paths within a single job and they will be automatically
concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`),
an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
## `dependencies`
......
# frozen_string_literal: true
module Gitlab
module Git
class DiffStatsCollection
include Enumerable
def initialize(diff_stats)
@collection = diff_stats
end
def each(&block)
@collection.each(&block)
end
end
end
end
......@@ -438,6 +438,16 @@ module Gitlab
Gitlab::Git::DiffCollection.new(iterator, options)
end
def diff_stats(left_id, right_id)
stats = wrapped_gitaly_errors do
gitaly_commit_client.diff_stats(left_id, right_id)
end
Gitlab::Git::DiffStatsCollection.new(stats)
rescue CommandError
Gitlab::Git::DiffStatsCollection.new([])
end
# Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present?
......
......@@ -172,6 +172,17 @@ module Gitlab
consume_commits_response(response)
end
def diff_stats(left_commit_sha, right_commit_sha)
request = Gitaly::DiffStatsRequest.new(
repository: @gitaly_repo,
left_commit_id: left_commit_sha,
right_commit_id: right_commit_sha
)
response = GitalyClient.call(@repository.storage, :diff_service, :diff_stats, request, timeout: GitalyClient.medium_timeout)
response.flat_map(&:stats)
end
def find_all_commits(opts = {})
request = Gitaly::FindAllCommitsRequest.new(
repository: @gitaly_repo,
......
import Vue from 'vue';
import { createStore } from '~/ide/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
import { file, resetStore } from '../helpers';
describe('IDE extra file row component', () => {
let Component;
let vm;
let unstagedFilesCount = 0;
let stagedFilesCount = 0;
let changesCount = 0;
beforeAll(() => {
Component = Vue.extend(FileRowExtra);
});
beforeEach(() => {
vm = createComponentWithStore(Component, createStore(), {
file: {
...file('test'),
},
mouseOver: false,
});
spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount);
spyOnProperty(vm, 'getStagedFilesCountForPath').and.returnValue(() => stagedFilesCount);
spyOnProperty(vm, 'getChangesInFolder').and.returnValue(() => changesCount);
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
stagedFilesCount = 0;
unstagedFilesCount = 0;
changesCount = 0;
});
describe('folderChangesTooltip', () => {
it('returns undefined when changes count is 0', () => {
expect(vm.folderChangesTooltip).toBe(undefined);
});
it('returns unstaged changes text', () => {
changesCount = 1;
unstagedFilesCount = 1;
expect(vm.folderChangesTooltip).toBe('1 unstaged change');
});
it('returns staged changes text', () => {
changesCount = 1;
stagedFilesCount = 1;
expect(vm.folderChangesTooltip).toBe('1 staged change');
});
it('returns staged and unstaged changes text', () => {
changesCount = 1;
stagedFilesCount = 1;
unstagedFilesCount = 1;
expect(vm.folderChangesTooltip).toBe('1 unstaged and 1 staged changes');
});
});
describe('show tree changes count', () => {
it('does not show for blobs', () => {
vm.file.type = 'blob';
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
});
it('does not show when changes count is 0', () => {
vm.file.type = 'tree';
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
});
it('does not show when tree is open', done => {
vm.file.type = 'tree';
vm.file.opened = true;
changesCount = 1;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
done();
});
});
it('shows for trees with changes', done => {
vm.file.type = 'tree';
vm.file.opened = false;
changesCount = 1;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
done();
});
});
});
describe('changes file icon', () => {
it('hides when file is not changed', () => {
expect(vm.$el.querySelector('.ide-file-changed-icon')).toBe(null);
});
it('shows when file is changed', done => {
vm.file.changed = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null);
done();
});
});
it('shows when file is staged', done => {
vm.file.staged = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null);
done();
});
});
it('shows when file is a tempFile', done => {
vm.file.tempFile = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null);
done();
});
});
});
describe('merge request icon', () => {
it('hides when not a merge request change', () => {
expect(vm.$el.querySelector('.ic-git-merge')).toBe(null);
});
it('shows when a merge request change', done => {
vm.file.mrChange = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null);
done();
});
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import repoFile from '~/ide/components/repo_file.vue';
import router from '~/ide/ide_router';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('RepoFile', () => {
let vm;
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
vm = createComponentWithStore(RepoFile, store, propsData);
vm.$mount();
}
afterEach(() => {
vm.$destroy();
});
it('renders link, icon and name', () => {
createComponent({
file: file('t4'),
level: 0,
});
const name = vm.$el.querySelector('.ide-file-name');
expect(name.href).toMatch('');
expect(name.textContent.trim()).toEqual(vm.file.name);
});
it('fires clickFile when the link is clicked', done => {
spyOn(router, 'push');
createComponent({
file: file('t3'),
level: 0,
});
vm.$el.querySelector('.file-name').click();
setTimeout(() => {
expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`);
done();
});
});
describe('folder', () => {
it('renders changes count inside folder', () => {
const f = {
...file('folder'),
path: 'testing',
type: 'tree',
branchId: 'master',
projectId: 'project',
};
store.state.changedFiles.push({
...file('fileName'),
path: 'testing/fileName',
});
createComponent({
file: f,
level: 0,
});
const treeChangesEl = vm.$el.querySelector('.ide-tree-changes');
expect(treeChangesEl).not.toBeNull();
expect(treeChangesEl.textContent).toContain('1');
});
it('renders action dropdown', done => {
createComponent({
file: {
...file('t4'),
type: 'tree',
branchId: 'master',
projectId: 'project',
},
level: 0,
});
setTimeout(() => {
expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull();
done();
});
});
});
describe('locked file', () => {
let f;
beforeEach(() => {
f = file('locked file');
f.file_lock = {
user: {
name: 'testuser',
updated_at: new Date(),
},
};
createComponent({
file: f,
level: 0,
});
});
it('renders lock icon', () => {
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => {
expect(
vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle,
).toContain('Locked by testuser');
});
});
it('calls scrollIntoView if made active', done => {
createComponent({
file: {
...file(),
type: 'blob',
active: false,
},
level: 0,
});
spyOn(vm, 'scrollIntoView');
vm.file.active = true;
vm.$nextTick(() => {
expect(vm.scrollIntoView).toHaveBeenCalled();
done();
});
});
});
import Vue from 'vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import { file } from 'spec/ide/helpers';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('RepoFile', () => {
let vm;
function createComponent(propsData) {
const FileRowComponent = Vue.extend(FileRow);
vm = mountComponent(FileRowComponent, propsData);
}
afterEach(() => {
vm.$destroy();
});
it('renders name', () => {
createComponent({
file: file('t4'),
level: 0,
});
const name = vm.$el.querySelector('.file-row-name');
expect(name.textContent.trim()).toEqual(vm.file.name);
});
it('emits toggleTreeOpen on click', () => {
createComponent({
file: {
...file('t3'),
type: 'tree',
},
level: 0,
});
spyOn(vm, '$emit').and.stub();
vm.$el.querySelector('.file-row').click();
expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path);
});
it('calls scrollIntoView if made active', done => {
createComponent({
file: {
...file(),
type: 'blob',
active: false,
},
level: 0,
});
spyOn(vm, 'scrollIntoView').and.stub();
vm.file.active = true;
vm.$nextTick(() => {
expect(vm.scrollIntoView).toHaveBeenCalled();
done();
});
});
it('indents row based on level', () => {
createComponent({
file: file('t4'),
level: 2,
});
expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px');
});
});
......@@ -1112,6 +1112,32 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
describe '#diff_stats' do
let(:left_commit_id) { 'feature' }
let(:right_commit_id) { 'master' }
it 'returns a DiffStatsCollection' do
collection = repository.diff_stats(left_commit_id, right_commit_id)
expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
expect(collection).to be_a(Enumerable)
end
it 'yields Gitaly::DiffStats objects' do
collection = repository.diff_stats(left_commit_id, right_commit_id)
expect(collection.to_a).to all(be_a(Gitaly::DiffStats))
end
it 'returns no Gitaly::DiffStats when SHAs are invalid' do
collection = repository.diff_stats('foo', 'bar')
expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
expect(collection).to be_a(Enumerable)
expect(collection.to_a).to be_empty
end
end
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
......
......@@ -118,6 +118,22 @@ describe Gitlab::GitalyClient::CommitService do
end
end
describe '#diff_stats' do
let(:left_commit_id) { 'master' }
let(:right_commit_id) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
it 'sends an RPC request' do
request = Gitaly::DiffStatsRequest.new(repository: repository_message,
left_commit_id: left_commit_id,
right_commit_id: right_commit_id)
expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:diff_stats)
.with(request, kind_of(Hash)).and_return([])
described_class.new(repository).diff_stats(left_commit_id, right_commit_id)
end
end
describe '#tree_entries' do
let(:path) { '/' }
......
......@@ -10,6 +10,12 @@
class MarkdownFeature
include FactoryBot::Syntax::Methods
attr_reader :fixture_path
def initialize(fixture_path = Rails.root.join('spec/fixtures/markdown.md.erb'))
@fixture_path = fixture_path
end
def user
@user ||= create(:user)
end
......@@ -130,7 +136,7 @@ class MarkdownFeature
end
def raw_markdown
markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb'))
markdown = File.read(fixture_path)
ERB.new(markdown).result(binding)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment