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
...@@ -36,7 +36,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -36,7 +36,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Release Scoping labels](#release-scoping-labels) - [Release Scoping labels](#release-scoping-labels)
- [Priority labels](#priority-labels) - [Priority labels](#priority-labels)
- [Severity labels](#severity-labels) - [Severity labels](#severity-labels)
- [Severity impact guidance](#severity-impact-guidance) - [Severity impact guidance](#severity-impact-guidance)
- [Label for community contributors](#label-for-community-contributors) - [Label for community contributors](#label-for-community-contributors)
- [Implement design & UI elements](#implement-design--ui-elements) - [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
...@@ -70,7 +70,7 @@ to contribute to GitLab in a way that is easy for everyone. ...@@ -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 For a first-time step-by-step guide to the contribution process, please see
["Contributing to GitLab"](https://about.gitlab.com/contributing/). ["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 GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial source edition, and GitLab Enterprise Edition (EE) which is our commercial
...@@ -151,8 +151,8 @@ the remaining issues on the GitHub issue tracker. ...@@ -151,8 +151,8 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute! ## I want to contribute!
If you want to contribute to GitLab [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] 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)]
is a great place to start. Issues with a lower weight (1 or 2) are deemed 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, 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 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 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 @@ ...@@ -2,15 +2,16 @@
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.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 NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
export default { export default {
components: { components: {
Icon, Icon,
RepoFile,
SkeletonLoadingContainer, SkeletonLoadingContainer,
NavDropdown, NavDropdown,
FileRow,
}, },
props: { props: {
viewerType: { viewerType: {
...@@ -34,8 +35,9 @@ export default { ...@@ -34,8 +35,9 @@ export default {
this.updateViewer(this.viewerType); this.updateViewer(this.viewerType);
}, },
methods: { methods: {
...mapActions(['updateViewer']), ...mapActions(['updateViewer', 'toggleTreeOpen']),
}, },
FileRowExtra,
}; };
</script> </script>
...@@ -63,11 +65,13 @@ export default { ...@@ -63,11 +65,13 @@ export default {
<div <div
class="ide-tree-body h-100" class="ide-tree-body h-100"
> >
<repo-file <file-row
v-for="file in currentTree.tree" v-for="file in currentTree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:extra-component="$options.FileRowExtra"
@toggleTreeOpen="toggleTreeOpen"
/> />
</div> </div>
</template> </template>
......
<script> <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 Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_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 { export default {
name: 'RepoFile', name: 'FileRow',
directives: {
tooltip,
},
components: { components: {
SkeletonLoadingContainer,
NewDropdown,
FileStatusIcon,
FileIcon, FileIcon,
ChangedFileIcon,
MrFileIcon,
Icon, Icon,
}, },
props: { props: {
...@@ -34,6 +17,11 @@ export default { ...@@ -34,6 +17,11 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
extraComponent: {
type: Object,
required: false,
default: null,
},
}, },
data() { data() {
return { return {
...@@ -41,34 +29,6 @@ export default { ...@@ -41,34 +29,6 @@ export default {
}; };
}, },
computed: { 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() { isTree() {
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
...@@ -83,17 +43,11 @@ export default { ...@@ -83,17 +43,11 @@ export default {
fileClass() { fileClass() {
return { return {
'file-open': this.isBlob && this.file.opened, 'file-open': this.isBlob && this.file.opened,
'file-active': this.isBlob && this.file.active, 'is-active': this.isBlob && this.file.active,
folder: this.isTree, folder: this.isTree,
'is-open': this.file.opened, '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: { watch: {
'file.active': function fileActiveWatch(active) { 'file.active': function fileActiveWatch(active) {
...@@ -108,14 +62,16 @@ export default { ...@@ -108,14 +62,16 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['toggleTreeOpen']), toggleTreeOpen(path) {
this.$emit('toggleTreeOpen', path);
},
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.isTree && this.hasUrlAtCurrentRoute()) { if (this.isTree && this.hasUrlAtCurrentRoute()) {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
router.push(`/project${this.file.url}`); if (this.$router) this.$router.push(`/project${this.file.url}`);
}, },
scrollIntoView(isInit = false) { scrollIntoView(isInit = false) {
const block = isInit && this.isTree ? 'center' : 'nearest'; const block = isInit && this.isTree ? 'center' : 'nearest';
...@@ -141,6 +97,8 @@ export default { ...@@ -141,6 +97,8 @@ export default {
return filePath === routePath; return filePath === routePath;
}, },
hasUrlAtCurrentRoute() { hasUrlAtCurrentRoute() {
if (!this.$router || !this.$router.currentRoute) return true;
return this.$router.currentRoute.path === `/project${this.file.url}`; return this.$router.currentRoute.path === `/project${this.file.url}`;
}, },
toggleHover(over) { toggleHover(over) {
...@@ -154,18 +112,18 @@ export default { ...@@ -154,18 +112,18 @@ export default {
<div> <div>
<div <div
:class="fileClass" :class="fileClass"
class="file" class="file-row"
role="button" role="button"
@click="clickFile" @click="clickFile"
@mouseover="toggleHover(true)" @mouseover="toggleHover(true)"
@mouseout="toggleHover(false)" @mouseout="toggleHover(false)"
> >
<div <div
class="file-name" class="file-row-name-container"
> >
<span <span
:style="levelIndentation" :style="levelIndentation"
class="ide-file-name str-truncated" class="file-row-name str-truncated"
> >
<file-icon <file-icon
:file-name="file.name" :file-name="file.name"
...@@ -175,53 +133,78 @@ export default { ...@@ -175,53 +133,78 @@ export default {
:size="16" :size="16"
/> />
{{ file.name }} {{ file.name }}
<file-status-icon
:file="file"
/>
</span> </span>
<span class="float-right ide-file-icon-holder"> <component
<mr-file-icon v-if="extraComponent"
v-if="file.mrChange" :is="extraComponent"
/> :file="file"
<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"
class="float-right"
/>
</span>
<new-dropdown
:type="file.type"
:path="file.path"
:mouse-over="mouseOver" :mouse-over="mouseOver"
class="float-right prepend-left-8"
/> />
</div> </div>
</div> </div>
<template v-if="file.opened"> <template v-if="file.opened">
<repo-file <file-row
v-for="childFile in file.tree" v-for="childFile in file.tree"
:key="childFile.key" :key="childFile.key"
:file="childFile" :file="childFile"
:level="level + 1" :level="level + 1"
:extra-component="extraComponent"
@toggleTreeOpen="toggleTreeOpen"
/> />
</template> </template>
</div> </div>
</template> </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; ...@@ -53,83 +53,9 @@ $ide-commit-header-height: 48px;
flex: 1; flex: 1;
min-height: 0; // firefox fix 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 { a {
color: $gl-text-color; color: $gl-text-color;
} }
th {
position: sticky;
top: 0;
}
}
.file-name {
display: flex;
overflow: visible;
align-items: center;
width: 100%;
} }
.multi-file-loading-container { .multi-file-loading-container {
...@@ -625,8 +551,7 @@ $ide-commit-header-height: 48px; ...@@ -625,8 +551,7 @@ $ide-commit-header-height: 48px;
} }
} }
.multi-file-commit-list-path, .multi-file-commit-list-path {
.ide-file-list .file {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: -$grid-size; margin-left: -$grid-size;
...@@ -634,28 +559,14 @@ $ide-commit-header-height: 48px; ...@@ -634,28 +559,14 @@ $ide-commit-header-height: 48px;
padding: $grid-size / 2 $grid-size; padding: $grid-size / 2 $grid-size;
border-radius: $border-radius-default; border-radius: $border-radius-default;
text-align: left; text-align: left;
&:hover,
&:focus {
background: $theme-gray-100;
}
&:active {
background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
cursor: pointer; cursor: pointer;
height: $ide-commit-row-height; height: $ide-commit-row-height;
padding-right: 0; padding-right: 0;
&.is-active {
background-color: $white-normal;
}
&:hover, &:hover,
&:focus { &:focus {
background: $theme-gray-100;
outline: 0; outline: 0;
.multi-file-discard-btn { .multi-file-discard-btn {
...@@ -665,6 +576,14 @@ $ide-commit-header-height: 48px; ...@@ -665,6 +576,14 @@ $ide-commit-header-height: 48px;
} }
} }
&:active {
background: $theme-gray-200;
}
&.is-active {
background-color: $white-normal;
}
svg { svg {
min-width: 16px; min-width: 16px;
vertical-align: middle; vertical-align: middle;
...@@ -1398,9 +1317,17 @@ $ide-commit-header-height: 48px; ...@@ -1398,9 +1317,17 @@ $ide-commit-header-height: 48px;
} }
} }
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle { .ide-new-btn {
color: $white-normal; display: none;
background-color: $blue-500;
.btn {
padding: 2px 5px;
}
.dropdown.show .ide-entry-dropdown-toggle {
color: $white-normal;
background-color: $blue-500;
}
} }
.ide-preview-header { .ide-preview-header {
...@@ -1465,3 +1392,28 @@ $ide-commit-header-height: 48px; ...@@ -1465,3 +1392,28 @@ $ide-commit-header-height: 48px;
width: $ide-commit-row-height; width: $ide-commit-row-height;
height: $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 { ...@@ -279,6 +279,10 @@ table.u2f-registrations {
} }
} }
.codes {
padding-top: 14px;
}
.oauth-application-show { .oauth-application-show {
.scope-name { .scope-name {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
......
...@@ -8,7 +8,10 @@ module Ci ...@@ -8,7 +8,10 @@ module Ci
include RedisCacheable include RedisCacheable
include ChronicDurationAttribute include ChronicDurationAttribute
include FromUnion include FromUnion
<<<<<<< HEAD
prepend EE::Ci::Runner prepend EE::Ci::Runner
=======
>>>>>>> upstream/master
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour ONLINE_CONTACT_TIMEOUT = 1.hour
......
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
class ProjectAuthorization < ActiveRecord::Base class ProjectAuthorization < ActiveRecord::Base
include FromUnion include FromUnion
<<<<<<< HEAD
prepend ::EE::ProjectAuthorization prepend ::EE::ProjectAuthorization
=======
>>>>>>> upstream/master
belongs_to :user belongs_to :user
belongs_to :project belongs_to :project
......
...@@ -21,8 +21,11 @@ class User < ActiveRecord::Base ...@@ -21,8 +21,11 @@ class User < ActiveRecord::Base
include WithUploads include WithUploads
include OptionallySearch include OptionallySearch
include FromUnion include FromUnion
<<<<<<< HEAD
prepend EE::User prepend EE::User
=======
>>>>>>> upstream/master
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
...@@ -364,10 +367,13 @@ class User < ActiveRecord::Base ...@@ -364,10 +367,13 @@ class User < ActiveRecord::Base
emails = emails.confirmed if confirmed emails = emails.confirmed if confirmed
from_union([users, emails]) from_union([users, emails])
<<<<<<< HEAD
end end
def existing_member?(email) def existing_member?(email)
User.where(email: email).any? || Email.where(email: email).any? User.where(email: email).any? || Email.where(email: email).any?
=======
>>>>>>> upstream/master
end end
def filter(filter_name) def filter(filter_name)
......
...@@ -10,4 +10,6 @@ ...@@ -10,4 +10,6 @@
%li %li
%span.monospace= code %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: ...@@ -1231,13 +1231,16 @@ rspec:
``` ```
The collected JUnit reports will be uploaded to GitLab as an artifact and will 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:** NOTE: **Note:**
In case the JUnit tool you use exports to multiple XML files, you can specify In case the JUnit tool you use exports to multiple XML files, you can specify
multiple test report paths within a single job multiple test report paths within a single job and they will be automatically
(`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`) and they will be automatically concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`),
concatenated into a single file. 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` ## `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 ...@@ -438,6 +438,16 @@ module Gitlab
Gitlab::Git::DiffCollection.new(iterator, options) Gitlab::Git::DiffCollection.new(iterator, options)
end 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 # Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha) def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present? raise ArgumentError, "sha can't be empty" unless sha.present?
......
...@@ -172,6 +172,17 @@ module Gitlab ...@@ -172,6 +172,17 @@ module Gitlab
consume_commits_response(response) consume_commits_response(response)
end 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 = {}) def find_all_commits(opts = {})
request = Gitaly::FindAllCommitsRequest.new( request = Gitaly::FindAllCommitsRequest.new(
repository: @gitaly_repo, 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 ...@@ -1112,6 +1112,32 @@ describe Gitlab::Git::Repository, :seed_helper do
end end
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 describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") } let(:master_file_paths) { repository.ls_files("master") }
let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") } let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
......
...@@ -118,6 +118,22 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -118,6 +118,22 @@ describe Gitlab::GitalyClient::CommitService do
end end
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 describe '#tree_entries' do
let(:path) { '/' } let(:path) { '/' }
......
...@@ -10,6 +10,12 @@ ...@@ -10,6 +10,12 @@
class MarkdownFeature class MarkdownFeature
include FactoryBot::Syntax::Methods 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 def user
@user ||= create(:user) @user ||= create(:user)
end end
...@@ -130,7 +136,7 @@ class MarkdownFeature ...@@ -130,7 +136,7 @@ class MarkdownFeature
end end
def raw_markdown def raw_markdown
markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb')) markdown = File.read(fixture_path)
ERB.new(markdown).result(binding) ERB.new(markdown).result(binding)
end end
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