Commit 35b5ea0a authored by Phil Hughes's avatar Phil Hughes

Merge branch 'issue-edit-inline' into issue-edit-inline-description-field-specs

[ci skip]
parents f80d2ab3 4fcff0bf
......@@ -16,10 +16,14 @@ class FilteredSearchManager {
this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
});
let recentSearchesKey = 'issue-recent-searches';
const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = searchHistoryDropdownElement ?
searchHistoryDropdownElement.dataset.projectFullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches';
if (page === 'merge_requests') {
recentSearchesKey = 'merge-request-recent-searches';
recentSearchesPagePrefix = 'merge-request-recent-searches';
}
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
// Fetch recent searches from localStorage
......@@ -47,7 +51,7 @@ class FilteredSearchManager {
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
this.recentSearchesService,
document.querySelector('.js-filtered-search-history-dropdown'),
searchHistoryDropdownElement,
);
this.recentSearchesRoot.init();
......
......@@ -3,6 +3,7 @@ import _ from 'underscore';
class RecentSearchesStore {
constructor(initialState = {}) {
this.state = Object.assign({
isLocalStorageAvailable: true,
recentSearches: [],
}, initialState);
}
......
......@@ -7,7 +7,7 @@ import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editActions from './edit_actions.vue';
import formComponent from './form.vue';
export default {
props: {
......@@ -60,19 +60,18 @@ export default {
return {
store,
state: store.state,
formState: store.formState,
showForm: false,
};
},
computed: {
elementType() {
return this.showForm ? 'form' : 'div';
formState() {
return this.store.formState;
},
},
components: {
descriptionComponent,
titleComponent,
editActions,
formComponent,
},
methods: {
openForm() {
......@@ -82,8 +81,11 @@ export default {
description: this.state.descriptionText,
};
},
closeForm() {
this.showForm = false;
},
updateIssuable() {
this.service.updateIssuable(this.formState)
this.service.updateIssuable(this.store.formState)
.then(() => {
eventHub.$emit('close.form');
})
......@@ -134,32 +136,38 @@ export default {
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$on('open.form', this.openForm);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
},
};
</script>
<template>
<div :is="elementType">
<title-component
:store="store"
:show-form="showForm"
:issuable-ref="issuableRef"
:title-html="state.titleHtml"
:title-text="state.titleText" />
<description-component
:store="store"
:show-form="showForm"
:can-update="canUpdate"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
<edit-actions
<div>
<form-component
v-if="canUpdate && showForm"
:can-destroy="canDestroy" />
:form-state="formState"
:can-destroy="canDestroy"
:markdown-docs="markdownDocs"
:markdown-preview-url="markdownPreviewUrl" />
<div v-else>
<title-component
:issuable-ref="issuableRef"
:title-html="state.titleHtml"
:title-text="state.titleText" />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus" />
</div>
</div>
</template>
<script>
import animateMixin from '../mixins/animate';
import descriptionField from './fields/description.vue';
export default {
mixins: [animateMixin],
......@@ -9,45 +8,32 @@
type: Boolean,
required: true,
},
store: {
type: Object,
descriptionHtml: {
type: String,
required: true,
},
showForm: {
type: Boolean,
descriptionText: {
type: String,
required: true,
},
markdownPreviewUrl: {
updatedAt: {
type: String,
required: true,
required: false,
default: '',
},
markdownDocs: {
taskStatus: {
type: String,
required: true,
required: false,
default: '',
},
},
data() {
return {
state: this.store.state,
preAnimation: false,
pulseAnimation: false,
timeAgoEl: $('.js-issue-edited-ago'),
};
},
computed: {
descriptionHtml() {
return this.state.descriptionHtml;
},
descriptionText() {
return this.state.descriptionText;
},
updatedAt() {
return this.state.updated_at;
},
taskStatus() {
return this.state.taskStatus;
},
},
watch: {
descriptionHtml() {
this.animateChange();
......@@ -91,9 +77,6 @@
}
},
},
components: {
descriptionField,
},
mounted() {
this.renderGFM();
},
......@@ -101,32 +84,25 @@
</script>
<template>
<div :class="{ 'common-note-form': showForm }">
<description-field
v-if="showForm"
:store="store"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
<div
v-if="descriptionHtml"
class="description"
:class="{
'js-task-list-container': canUpdate
}">
<div
v-else-if="descriptionHtml"
class="description"
class="wiki"
:class="{
'js-task-list-container': canUpdate
}">
<div
class="wiki"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="descriptionHtml"
ref="gfm-content">
</div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText">
</textarea>
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="descriptionHtml"
ref="gfm-content">
</div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText">
</textarea>
</div>
</template>
......@@ -4,7 +4,7 @@
export default {
props: {
store: {
formState: {
type: Object,
required: true,
},
......@@ -17,11 +17,6 @@
required: true,
},
},
data() {
return {
state: this.store.formState,
};
},
components: {
markdownField,
},
......@@ -29,7 +24,7 @@
</script>
<template>
<div>
<div class="common-note-form">
<label
class="sr-only"
for="issue-description">
......@@ -43,7 +38,7 @@
class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-slash-commands="false"
aria-label="Description"
v-model="state.description"
v-model="formState.description"
ref="textatea"
slot="textarea">
</textarea>
......
<script>
export default {
props: {
store: {
formState: {
type: Object,
required: true,
},
},
data() {
return {
state: this.store.formState,
};
},
};
</script>
......@@ -27,6 +22,6 @@
type="text"
placeholder="Issue title"
aria-label="Issue title"
v-model="state.title" />
v-model="formState.title" />
</fieldset>
</template>
<script>
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
export default {
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
markdownPreviewUrl: {
type: String,
required: true,
},
markdownDocs: {
type: String,
required: true,
},
},
components: {
titleField,
descriptionField,
editActions,
},
};
</script>
<template>
<form>
<title-field
:form-state="formState" />
<description-field
:form-state="formState"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
<edit-actions
:can-destroy="canDestroy" />
</form>
</template>
<script>
import animateMixin from '../mixins/animate';
import titleField from './fields/title.vue';
export default {
mixins: [animateMixin],
components: {
titleField,
},
data() {
return {
preAnimation: false,
......@@ -27,14 +23,6 @@
type: String,
required: true,
},
store: {
type: Object,
required: true,
},
showForm: {
type: Boolean,
required: true,
},
},
watch: {
titleHtml() {
......@@ -53,19 +41,13 @@
</script>
<template>
<div>
<title-field
v-if="showForm"
:store="store" />
<h2
v-else
class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
</div>
<h2
class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
</template>
......@@ -8,15 +8,15 @@ export default class Service {
this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint, {}, {
rendered_title: {
realtimeChanges: {
method: 'GET',
url: `${this.endpoint}/rendered_title`,
url: `${this.endpoint}/realtime_changes`,
},
});
}
getData() {
return this.resource.rendered_title();
return this.resource.realtimeChanges();
}
deleteIssuable() {
......
......@@ -24,7 +24,7 @@ const normalizeNewlines = function(str) {
(function() {
this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_SLASH_COMMANDS = /^\/\w+/gm;
const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm;
Notes.interval = null;
......@@ -1170,6 +1170,7 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
......@@ -1183,14 +1184,11 @@ const normalizeNewlines = function(str) {
<span class="hidden-xs">${currentUserFullname}</span>
<span class="note-headline-light">@${currentUsername}</span>
</a>
<span class="note-headline-light">
<i class="fa fa-spinner fa-spin" aria-label="Comment is being posted" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="note-body">
<div class="note-text">
<p>${formContent}</p>
<p>${escapedFormContent}</p>
</div>
</div>
</div>
......
......@@ -60,7 +60,7 @@
},
mounted() {
/*
GLForm class handles all the toolbar buttons etc.
GLForm class handles all the toolbar buttons
*/
return new gl.GLForm($(this.$refs['gl-form']));
},
......
<script>
import tooltipMixin from '../../mixins/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
......@@ -82,9 +86,9 @@
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
data-toggle="tooltip"
title="Go full screen"
type="button">
type="button"
ref="tooltip">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">
......
<script>
import tooltipMixin from '../../mixins/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
......@@ -24,6 +29,11 @@
default: false,
},
},
computed: {
iconClass() {
return `fa-${this.icon}`;
},
},
};
</script>
......@@ -32,17 +42,17 @@
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
data-container="body"
data-toggle="tooltip"
:title="buttonTitle"
:aria-label="buttonTitle">
<i
aria-hidden="true"
class="fa fa-fw"
:class="'fa-' + icon">
:class="iconClass">
</i>
</button>
</template>
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
this.$nextTick(() => {
$(this.$refs.tooltip).tooltip();
});
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
this.$nextTick(() => {
$(this.$refs.tooltip).tooltip('fixTitle');
});
},
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
};
......@@ -24,10 +24,10 @@
}
@mixin scrolling-links() {
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
&::-webkit-scrollbar {
display: none;
......@@ -35,6 +35,7 @@
}
.nav-links {
display: flex;
padding: 0;
margin: 0;
list-style: none;
......@@ -42,17 +43,16 @@
border-bottom: 1px solid $border-color;
li {
display: inline-block;
display: flex;
a {
display: inline-block;
padding: $gl-btn-padding;
padding-bottom: 11px;
margin-bottom: -1px;
font-size: 14px;
line-height: 28px;
color: $gl-text-color-secondary;
border-bottom: 2px solid transparent;
white-space: nowrap;
&:hover,
&:active,
......@@ -85,10 +85,10 @@
.container-fluid {
background-color: $gray-normal;
margin-bottom: 0;
display: flex;
}
li {
&.active a {
border-bottom: none;
color: $link-underline-blue;
......@@ -137,9 +137,9 @@
}
.nav-links {
display: inline-block;
margin-bottom: 0;
border-bottom: none;
float: left;
&.wide {
width: 100%;
......@@ -337,6 +337,10 @@
border-bottom: none;
height: 51px;
@media (min-width: $screen-sm-min) {
justify-content: center;
}
li {
a {
padding-top: 10px;
......@@ -347,6 +351,7 @@
.scrolling-tabs-container {
position: relative;
overflow: hidden;
.nav-links {
@include scrolling-links();
......@@ -484,10 +489,7 @@
.inner-page-scroll-tabs {
position: relative;
.nav-links {
padding-bottom: 1px;
}
overflow: hidden;
.fade-right {
@include fade(left, $white-light);
......
......@@ -53,6 +53,7 @@
.right-sidebar-expanded {
padding-right: 0;
z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
......
......@@ -378,7 +378,7 @@
background-color: $row-hover;
}
.fa-spinner {
.fa-refresh {
font-size: 13px;
margin-left: 3px;
}
......
......@@ -23,16 +23,6 @@
.merge-manually {
@extend .fixed-width-container;
}
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
}
}
.merge-request-details {
......@@ -206,7 +196,7 @@
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
z-index: 2;
z-index: 200;
&.right-sidebar-expanded {
width: $gutter_width;
......
......@@ -121,6 +121,7 @@
.dropdown-menu {
margin-top: 11px;
z-index: 200;
}
.ci-action-icon-wrapper {
......@@ -690,8 +691,9 @@
.merge-request-tabs-holder {
top: $header-height;
z-index: 10;
z-index: 100;
background-color: $white-light;
border-bottom: 1px solid $border-color;
@media(min-width: $screen-sm-min) {
position: sticky;
......@@ -711,6 +713,16 @@
padding-right: $gl-padding;
}
}
.nav-links {
border: 0;
}
}
.merge-request-tabs {
display: flex;
margin-bottom: 0;
padding: 0;
}
.limit-container-width {
......@@ -721,6 +733,15 @@
}
}
.merge-request-tabs-container {
display: flex;
justify-content: space-between;
@media (max-width: $screen-xs-max) {
flex-direction: column-reverse;
}
}
.limit-container-width:not(.container-limited) {
.merge-request-tabs-holder:not(.affix) {
.merge-request-tabs-container {
......
......@@ -609,6 +609,15 @@ ul.notes {
}
.line-resolve-all-container {
@media (min-width: $screen-sm-min) {
margin-right: 0;
padding-left: $gl-padding;
}
> div {
white-space: nowrap;
}
.btn-group {
margin-left: -4px;
}
......
......@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch, :rendered_title, :create_merge_request]
:related_branches, :can_create_branch, :realtime_changes, :create_merge_request]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show, :rendered_title]
before_action :authorize_read_issue!, only: [:show, :realtime_changes]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
......@@ -199,7 +199,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def rendered_title
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
......
File mode changed from 100755 to 100644
# UsersFinder
#
# Used to filter users by set of params
#
# Arguments:
# current_user - which user use
# params:
# username: string
# extern_uid: string
# provider: string
# search: string
# active: boolean
# blocked: boolean
# external: boolean
#
class UsersFinder
attr_accessor :current_user, :params
def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
def execute
users = User.all
users = by_username(users)
users = by_search(users)
users = by_blocked(users)
users = by_active(users)
users = by_external_identity(users)
users = by_external(users)
users
end
private
def by_username(users)
return users unless params[:username]
users.where(username: params[:username])
end
def by_search(users)
return users unless params[:search].present?
users.search(params[:search])
end
def by_blocked(users)
return users unless params[:blocked]
users.blocked
end
def by_active(users)
return users unless params[:active]
users.active
end
def by_external_identity(users)
return users unless current_user.admin? && params[:extern_uid] && params[:provider]
users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
end
def by_external(users)
return users = users.where.not(external: true) unless current_user.admin?
return users unless params[:external]
users.external
end
end
......@@ -7,6 +7,10 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
if url == '.' || url == './'
url = File.join(Gitlab.config.gitlab.url, @project.full_path)
end
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2
project.sub!(/\.git\z/, '')
......
......@@ -292,7 +292,7 @@ class Issue < ActiveRecord::Base
end
def expire_etag_cache
key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
key = Gitlab::Routing.url_helpers.realtime_changes_namespace_project_issue_path(
project.namespace,
project,
self
......
......@@ -1163,8 +1163,6 @@ class Repository
@project.repository_storage_path
end
delegate :gitaly_channel, :gitaly_repository, to: :raw_repository
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end
......
......@@ -136,7 +136,7 @@
- else
= build.id
- if build.retried?
%i.fa.fa-spinner.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
:javascript
new Sidebar();
......@@ -27,40 +27,42 @@
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.merge-request-tabs.nav-links.scrolling-tabs
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
%span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
Commits
%span.badge= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
Pipelines
%span.badge= @pipelines.size
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes
%span.badge= @merge_request.diff_size
%li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
%div
.line-resolve-all{ "v-show" => "discussionCount > 0",
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
= render "discussions/jump_to_next"
.merge-request-tabs-container
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
.nav-links.scrolling-tabs
%ul.merge-request-tabs
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
%span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
Commits
%span.badge= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
Pipelines
%span.badge= @pipelines.size
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes
%span.badge= @merge_request.diff_size
#resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
%div
.line-resolve-all{ "v-show" => "discussionCount > 0",
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
= render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
......
......@@ -19,7 +19,7 @@
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
.js-filtered-search-history-dropdown
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
.filtered-search-box-input-container
.scroll-container
%ul.tokens-container.list-unstyled
......
---
title: prevent nav tabs from wrapping to new line
merge_request:
author:
---
title: Remove spinner from loading comment
merge_request:
author:
---
title: Scope issue/merge request recent searches to project
merge_request:
author:
---
title: Allow GitLab instance to start when InfluxDB hostname cannot be resolved
merge_request: 11356
author:
---
title: Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL
merge_request: 11034
author:
---
title: Enable cancelling non-HEAD pending pipelines by default for all projects
merge_request: 11023
author:
---
title: Fix token interpolation when setting the Github remote
merge_request:
author:
---
title: 'Repository browser: handle in-repository submodule urls'
merge_request:
author: David Turner
......@@ -16,7 +16,7 @@ Rails.application.configure do
# config.assets.css_compressor = :sass
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = true
config.assets.compile = false
# Generate digests for assets URLs
config.assets.digest = true
......
require 'uri'
# Make sure we initialize our Gitaly channels before Sidekiq starts multi-threaded execution.
if Gitlab.config.gitaly.enabled || Rails.env.test?
Gitlab::GitalyClient.configure_channels
Gitlab.config.repositories.storages.keys.each do |storage|
# Force validation of each address
Gitlab::GitalyClient.address(storage)
end
end
......@@ -244,7 +244,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests
get :related_branches
get :can_create_branch
get :rendered_title
get :realtime_changes
post :create_merge_request
end
collection do
......
class MakeAutoCancelPendingPipelinesOnByDefault < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
change_column_default(:projects, :auto_cancel_pending_pipelines, 1)
end
def down
change_column_default(:projects, :auto_cancel_pending_pipelines, 0)
end
end
class CreateIndexCiPipelinesAutoCanceledById < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# MySQL would already have the index
unless index_exists?(:ci_pipelines, :auto_canceled_by_id)
add_concurrent_index(:ci_pipelines, :auto_canceled_by_id)
end
end
def down
# We cannot remove index for MySQL because it's needed for foreign key
if Gitlab::Database.postgresql?
remove_concurrent_index(:ci_pipelines, :auto_canceled_by_id)
end
end
end
class CreateIndexCiBuildsAutoCanceledById < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# MySQL would already have the index
unless index_exists?(:ci_builds, :auto_canceled_by_id)
add_concurrent_index(:ci_builds, :auto_canceled_by_id)
end
end
def down
# We cannot remove index for MySQL because it's needed for foreign key
if Gitlab::Database.postgresql?
remove_concurrent_index(:ci_builds, :auto_canceled_by_id)
end
end
end
class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1)
end
def down
# Nothing we can do!
end
end
......@@ -235,6 +235,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do
t.boolean "retried"
end
add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
......@@ -284,6 +285,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do
t.integer "pipeline_schedule_id"
end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
......@@ -986,7 +988,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.integer "auto_cancel_pending_pipelines", default: 0, null: false
t.integer "auto_cancel_pending_pipelines", default: 1, null: false
t.string "import_jid"
t.integer "cached_markdown_version"
t.datetime "last_repository_updated_at"
......
# Installing Git
> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** user guide ||
> **Level:** beginner ||
> **Author:** [Sean Packham](https://gitlab.com/SeanPackham) ||
> **Publication date:** 2017/05/15
To begin contributing to GitLab projects
you will need to install the Git client on your computer.
This article will show you how to install Git on macOS, Ubuntu Linux and Windows.
## Install Git on macOS using the Homebrew package manager
Although it is easy to use the version of Git shipped with macOS
or install the latest version of Git on macOS by downloading it from the project website,
we recommend installing it via Homebrew to get access to
an extensive selection of dependancy managed libraries and applications.
If you are sure you don't need access to any additional development libraries
or don't have approximately 15gb of available disk space for Xcode and Homebrew
use one of the the aforementioned methods.
### Installing Xcode
Xcode is needed by Homebrew to build dependencies.
You can install [XCode](https://developer.apple.com/xcode/)
through the macOS App Store.
### Installing Homebrew
Once Xcode is installed browse to the [Homebrew website](http://brew.sh/index.html)
for the official Homebrew installation instructions.
### Installing Git via Homebrew
With Homebrew installed you are now ready to install Git.
Open a Terminal and enter in the following command:
```bash
brew install git
```
Congratulations you should now have Git installed via Homebrew.
Next read our article on [adding an SSH key to GitLab](../../ssh/README.md).
## Install Git on Ubuntu Linux
On Ubuntu and other Linux operating systems
it is recommended to use the built in package manager to install Git.
Open a Terminal and enter in the following commands
to install the latest Git from the official Git maintained package archives:
```bash
sudo apt-add-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git
```
Congratulations you should now have Git installed via the Ubuntu package manager.
Next read our article on [adding an SSH key to GitLab](../../ssh/README.md).
## Installing Git on Windows from the Git website
Browse to the [Git website](https://git-scm.com/) and download and install Git for Windows.
Next read our article on [adding an SSH key to GitLab](../../ssh/README.md).
......@@ -12,6 +12,10 @@ They are written by members of the GitLab Team and by
- **LDAP**
- [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)
## Git
- [How to install Git](how_to_install_git/index.md)
## GitLab Pages
- **GitLab Pages from A to Z**
......
......@@ -155,7 +155,7 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to
**Settings ➔ Runners**. Setting up a Runner is easy and straightforward. The
**Settings ➔ CI/CD Pipelines**. Setting up a Runner is easy and straightforward. The
official Runner supported by GitLab is written in Go and its documentation
can be found at <https://docs.gitlab.com/runner/>.
......@@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
Once the Runner has been set up, you should see it on the Runners page of your
project, following **Settings ➔ Runners**.
project, following **Settings ➔ CI/CD Pipelines**.
![Activated runners](img/runners_activated.png)
......@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can
build any project.
To enable the **Shared Runners** you have to go to your project's
**Settings ➔ Runners** and click **Enable shared runners**.
**Settings ➔ CI/CD Pipelines** and click **Enable shared runners**.
[Read more on Shared Runners](../runners/README.md).
......
......@@ -392,7 +392,7 @@ Once you [have configured](#configuration) GitLab in your `values.yml` file,
run the following:
```bash
helm install --namepace <NAMEPACE> --name gitlab -f <CONFIG_VALUES_FILE> gitlab/gitlab
helm install --namespace <NAMESPACE> --name gitlab -f <CONFIG_VALUES_FILE> gitlab/gitlab
```
where:
......@@ -407,7 +407,7 @@ Once your GitLab Chart is installed, configuration changes and chart updates
should we done using `helm upgrade`
```bash
helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
```
where:
......
......@@ -22,6 +22,7 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **Articles:**
- [How to install Git](../../articles/how_to_install_git/index.md)
- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
- **Presentations:**
......
......@@ -56,16 +56,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
users = User.all
users = User.where(username: params[:username]) if params[:username]
users = users.active if params[:active]
users = users.search(params[:search]) if params[:search].present?
users = users.blocked if params[:blocked]
if current_user.admin?
users = users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) if params[:extern_uid] && params[:provider]
users = users.external if params[:external]
end
users = UsersFinder.new(current_user, params).execute
entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic
present paginate(users), with: entity
......
require_relative 'error'
module Github
class Import
include Gitlab::ShellAdapter
......@@ -6,6 +7,7 @@ module Github
class MergeRequest < ::MergeRequest
self.table_name = 'merge_requests'
self.reset_callbacks :create
self.reset_callbacks :save
self.reset_callbacks :commit
self.reset_callbacks :update
......@@ -16,6 +18,7 @@ module Github
self.table_name = 'issues'
self.reset_callbacks :save
self.reset_callbacks :create
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
......@@ -79,7 +82,7 @@ module Github
def fetch_repository
begin
project.create_repository unless project.repository.exists?
project.repository.add_remote('github', "https://{options.fetch(:token)}@github.com/#{repo}.git")
project.repository.add_remote('github', "https://#{options.fetch(:token)}@github.com/#{repo}.git")
project.repository.set_remote_as_mirror('github')
project.repository.fetch_remote('github', forced: true)
rescue Gitlab::Shell::Error => e
......
......@@ -283,7 +283,6 @@ module Gitlab
add_column(table, new, new_type,
limit: old_col.limit,
null: old_col.null,
precision: old_col.precision,
scale: old_col.scale)
......@@ -307,6 +306,8 @@ module Gitlab
update_column_in_batches(table, new, Arel::Table.new(table)[old])
change_column_null(table, new, false) unless old_col.null
copy_indexes(table, old, new)
copy_foreign_keys(table, old, new)
end
......
module Gitlab
module DependencyLinker
class BaseLinker
def self.link(plain_text, highlighted_text)
new(plain_text, highlighted_text).link
class_attribute :file_type
def self.support?(blob_name)
Gitlab::FileDetector.type_of(blob_name) == file_type
end
def self.link(*args)
new(*args).link
end
attr_accessor :plain_text, :highlighted_text
......
module Gitlab
module DependencyLinker
class GemfileLinker < BaseLinker
def self.support?(blob_name)
blob_name == 'Gemfile' || blob_name == 'gems.rb'
end
self.file_type = :gemfile
private
......
......@@ -7,8 +7,8 @@ module Gitlab
# - Don't contain a reserved word (expect for the words used in the
# regex itself)
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/rendered_title` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues rendered_title
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze
RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
......@@ -18,7 +18,7 @@ module Gitlab
'issue_notes'
),
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/rendered_title\z),
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/realtime_changes\z),
'issue_title'
),
Gitlab::EtagCaching::Router::Route.new(
......
......@@ -12,6 +12,7 @@ module Gitlab
version: 'version',
gitignore: '.gitignore',
koding: '.koding.yml',
gemfile: /\A(Gemfile|gems\.rb)\z/,
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/,
route_map: 'route-map.yml'
......
......@@ -27,13 +27,15 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
attr_reader :storage
# 'path' must be the path to a _bare_ git repository, e.g.
# /path/to/my-repo.git
def initialize(repository_storage, relative_path)
@repository_storage = repository_storage
def initialize(storage, relative_path)
@storage = storage
@relative_path = relative_path
storage_path = Gitlab.config.repositories.storages[@repository_storage]['path']
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@name = @relative_path.split("/").last
@attributes = Gitlab::Git::Attributes.new(path)
......@@ -114,7 +116,7 @@ module Gitlab
# Returns the number of valid branches
def branch_count
Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
gitaly_migrate(:branch_names) do |is_enabled|
if is_enabled
gitaly_ref_client.count_branch_names
else
......@@ -133,7 +135,7 @@ module Gitlab
# Returns the number of valid tags
def tag_count
Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
gitaly_migrate(:tag_names) do |is_enabled|
if is_enabled
gitaly_ref_client.count_tag_names
else
......@@ -471,7 +473,7 @@ module Gitlab
def ref_name_for_sha(ref_path, sha)
# NOTE: This feature is intentionally disabled until
# https://gitlab.com/gitlab-org/gitaly/issues/180 is resolved
# Gitlab::GitalyClient.migrate(:find_ref_name) do |is_enabled|
# gitaly_migrate(:find_ref_name) do |is_enabled|
# if is_enabled
# gitaly_ref_client.find_ref_name(sha, ref_path)
# else
......@@ -965,11 +967,7 @@ module Gitlab
end
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@repository_storage, @relative_path)
end
def gitaly_channel
Gitlab::GitalyClient.get_channel(@repository_storage)
Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
end
private
......
......@@ -4,56 +4,42 @@ module Gitlab
module GitalyClient
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
# This function is not thread-safe because it updates Hashes in instance variables.
def self.configure_channels
@addresses = {}
@channels = {}
Gitlab.config.repositories.storages.each do |name, params|
address = params['gitaly_address']
unless address.present?
raise "storage #{name.inspect} is missing a gitaly_address"
end
MUTEX = Mutex.new
private_constant :MUTEX
unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
def self.stub(name, storage)
MUTEX.synchronize do
@stubs ||= {}
@stubs[storage] ||= {}
@stubs[storage][name] ||= begin
klass = Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub)
addr = address(storage)
addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp'
klass.new(addr, :this_channel_is_insecure)
end
@addresses[name] = address
@channels[name] = new_channel(address)
end
end
def self.new_channel(address)
address = address.sub(%r{^tcp://}, '') if URI(address).scheme == 'tcp'
# NOTE: When Gitaly runs on a Unix socket, permissions are
# handled using the file system and no additional authentication is
# required (therefore the :this_channel_is_insecure flag)
# TODO: Add authentication support when Gitaly is running on a TCP socket.
GRPC::Core::Channel.new(address, {}, :this_channel_is_insecure)
def self.clear_stubs!
MUTEX.synchronize do
@stubs = nil
end
end
def self.get_channel(storage)
if !Rails.env.production? && @channels.nil?
# In development mode the Rails auto-loader may reset the instance
# variables of this class. What we do here is not thread-safe. In normal
# circumstances (including production) these instance variables have
# been initialized from config/initializers.
configure_channels
end
def self.address(storage)
params = Gitlab.config.repositories.storages[storage]
raise "storage not found: #{storage.inspect}" if params.nil?
@channels[storage]
end
address = params['gitaly_address']
unless address.present?
raise "storage #{storage.inspect} is missing a gitaly_address"
end
def self.get_address(storage)
if !Rails.env.production? && @addresses.nil?
# In development mode the Rails auto-loader may reset the instance
# variables of this class. What we do here is not thread-safe. In normal
# circumstances (including development) these instance variables have
# been initialized from config/initializers.
configure_channels
unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
end
@addresses[storage]
address
end
def self.enabled?
......
......@@ -11,7 +11,7 @@ module Gitlab
end
def is_ancestor(ancestor_id, child_id)
stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: @repository.gitaly_channel)
stub = GitalyClient.stub(:commit, @repository.storage)
request = Gitaly::CommitIsAncestorRequest.new(
repository: @gitaly_repo,
ancestor_id: ancestor_id,
......@@ -52,7 +52,7 @@ module Gitlab
end
def diff_service_stub
Gitaly::Diff::Stub.new(nil, nil, channel_override: @repository.gitaly_channel)
GitalyClient.stub(:diff, @repository.storage)
end
end
end
......
......@@ -6,7 +6,7 @@ module Gitlab
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
@stub = GitalyClient.stub(:notifications, repository.storage)
end
def post_receive
......
......@@ -6,7 +6,7 @@ module Gitlab
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@stub = Gitaly::Ref::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
@stub = GitalyClient.stub(:ref, repository.storage)
end
def default_branch_name
......
......@@ -49,6 +49,9 @@ module Gitlab
end
end
end
rescue Errno::EADDRNOTAVAIL, SocketError => ex
Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.')
Gitlab::EnvironmentLogger.error(ex)
end
def self.prepare_metrics(metrics)
......
......@@ -26,7 +26,7 @@ module Gitlab
}
if Gitlab.config.gitaly.enabled
address = Gitlab::GitalyClient.get_address(project.repository_storage)
address = Gitlab::GitalyClient.address(project.repository_storage)
params[:Repository] = repository.gitaly_repository.to_h
feature_enabled = case action.to_s
......
......@@ -5,14 +5,7 @@ describe 'Auto deploy' do
let(:project) { create(:project, :repository) }
before do
project.create_kubernetes_service(
active: true,
properties: {
namespace: project.path,
api_url: 'https://kubernetes.example.com',
token: 'a' * 40
}
)
create :kubernetes_service, project: project
project.team << [user, :master]
login_as user
end
......
......@@ -14,7 +14,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end
it 'shows a button to resolve all discussions by creating a new issue' do
within('li#resolve-count-app') do
within('#resolve-count-app') do
expect(page).to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
......
......@@ -3,17 +3,17 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) }
let(:project_1) { create(:empty_project, :public) }
let(:project_2) { create(:empty_project, :public) }
let(:project_1_local_storage_key) { "#{project_1.full_path}-issue-recent-searches" }
before do
Capybara.ignore_hidden_elements = false
project.add_master(user)
group.add_developer(user)
create(:issue, project: project)
login_as(user)
create(:issue, project: project_1)
create(:issue, project: project_2)
# Visit any fast-loading page so we can clear local storage without a DOM exception
visit '/404'
remove_recent_searches
end
......@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do
end
it 'searching adds to recent searches' do
visit namespace_project_issues_path(project.namespace, project)
visit namespace_project_issues_path(project_1.namespace, project_1)
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
......@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'visiting URL with search params adds to recent searches' do
visit namespace_project_issues_path(project.namespace, project, label_name: 'foo', search: 'bar')
visit namespace_project_issues_path(project.namespace, project, label_name: 'qux', search: 'garply')
visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'foo', search: 'bar')
visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'qux', search: 'garply')
items = all('.filtered-search-history-dropdown-item', visible: false)
......@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do
end
it 'saved recent searches are restored last on the list' do
set_recent_searches('["saved1", "saved2"]')
set_recent_searches(project_1_local_storage_key, '["saved1", "saved2"]')
visit namespace_project_issues_path(project.namespace, project, search: 'foo')
visit namespace_project_issues_path(project_1.namespace, project_1, search: 'foo')
items = all('.filtered-search-history-dropdown-item', visible: false)
......@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do
expect(items[2].text).to eq('saved2')
end
it 'searches are scoped to projects' do
visit namespace_project_issues_path(project_1.namespace, project_1)
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
visit namespace_project_issues_path(project_2.namespace, project_2)
input_filtered_search('more', submit: true)
input_filtered_search('things', submit: true)
items = all('.filtered-search-history-dropdown-item', visible: false)
expect(items.count).to eq(2)
expect(items[0].text).to eq('things')
expect(items[1].text).to eq('more')
end
it 'clicking item fills search input' do
set_recent_searches('["foo", "bar"]')
visit namespace_project_issues_path(project.namespace, project)
set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit namespace_project_issues_path(project_1.namespace, project_1)
all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
wait_for_filtered_search('foo')
......@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'clear recent searches button, clears recent searches' do
set_recent_searches('["foo"]')
visit namespace_project_issues_path(project.namespace, project)
set_recent_searches(project_1_local_storage_key, '["foo"]')
visit namespace_project_issues_path(project_1.namespace, project_1)
items_before = all('.filtered-search-history-dropdown-item', visible: false)
......@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'shows flash error when failed to parse saved history' do
set_recent_searches('fail')
visit namespace_project_issues_path(project.namespace, project)
set_recent_searches(project_1_local_storage_key, 'fail')
visit namespace_project_issues_path(project_1.namespace, project_1)
expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches')
end
......
require 'spec_helper'
describe UsersFinder do
describe '#execute' do
let!(:user1) { create(:user, username: 'johndoe') }
let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
let!(:external_user) { create(:user, :external) }
let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
context 'with a normal user' do
let(:user) { create(:user) }
it 'returns all users' do
users = described_class.new(user).execute
expect(users).to contain_exactly(user, user1, user2, omniauth_user)
end
it 'filters by username' do
users = described_class.new(user, username: 'johndoe').execute
expect(users).to contain_exactly(user1)
end
it 'filters by search' do
users = described_class.new(user, search: 'orando').execute
expect(users).to contain_exactly(user2)
end
it 'filters by blocked users' do
users = described_class.new(user, blocked: true).execute
expect(users).to contain_exactly(user2)
end
it 'filters by active users' do
users = described_class.new(user, active: true).execute
expect(users).to contain_exactly(user, user1, omniauth_user)
end
it 'returns no external users' do
users = described_class.new(user, external: true).execute
expect(users).to contain_exactly(user, user1, user2, omniauth_user)
end
end
context 'with an admin user' do
let(:admin) { create(:admin) }
it 'filters by external users' do
users = described_class.new(admin, external: true).execute
expect(users).to contain_exactly(external_user)
end
it 'returns all users' do
users = described_class.new(admin).execute
expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user)
end
end
end
end
......@@ -81,6 +81,19 @@ describe SubmoduleHelper do
end
end
context 'in-repository submodule' do
let(:group) { create(:group, name: "Master Project", path: "master-project") }
let(:project) { create(:empty_project, group: group) }
before do
self.instance_variable_set(:@project, project)
end
it 'in-repository' do
stub_url('./')
expect(submodule_links(submodule_item)).to eq(["/master-project/#{project.path}", "/master-project/#{project.path}/tree/hash"])
end
end
context 'submodule on gitlab.com' do
it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
......
......@@ -29,7 +29,7 @@ describe('Issuable output', () => {
propsData: {
canUpdate: true,
canDestroy: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1',
initialTitle: '',
initialDescriptionHtml: '',
......@@ -75,18 +75,6 @@ describe('Issuable output', () => {
});
});
it('changes element to `form` when open', (done) => {
vm.showForm = true;
Vue.nextTick(() => {
expect(
vm.$el.tagName,
).toBe('FORM');
done();
});
});
it('does not show actions if permissions are incorrect', (done) => {
vm.showForm = true;
vm.canUpdate = false;
......
......@@ -17,7 +17,7 @@ describe('Title field component', () => {
vm = new Component({
propsData: {
store,
formState: store.formState,
},
}).$mount();
});
......
......@@ -7,17 +7,18 @@ describe('Title component', () => {
beforeEach(() => {
const Component = Vue.extend(titleComponent);
const store = new Store({
titleHtml: '',
descriptionHtml: '',
issuableRef: '',
});
vm = new Component({
propsData: {
issuableRef: '#1',
titleHtml: 'Testing <img />',
titleText: 'Testing',
showForm: false,
store: new Store({
titleHtml: '',
descriptionHtml: '',
issuableRef: '',
}),
formState: store.formState,
},
}).$mount();
});
......
......@@ -377,7 +377,7 @@ import '~/notes';
});
it('should return true when comment begins with a slash command', () => {
const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this';
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeTruthy();
......@@ -401,10 +401,18 @@ import '~/notes';
describe('stripSlashCommands', () => {
it('should strip slash commands from the comment which begins with a slash command', () => {
this.notes = new Notes();
const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this';
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment);
expect(stripedComment).not.toBe(sampleComment);
expect(stripedComment).toBe('');
});
it('should strip slash commands from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment);
expect(stripedComment).toBe('Merging this');
});
it('should NOT strip string that has slashes within', () => {
......@@ -424,6 +432,22 @@ import '~/notes';
beforeEach(() => {
this.notes = new Notes('', []);
spyOn(_, 'escape').and.callFake((comment) => {
const escapedString = comment.replace(/["&'<>]/g, (a) => {
const escapedToken = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;'
}[a];
return escapedToken;
});
return escapedString;
});
});
it('should return constructed placeholder element for regular note based on form contents', () => {
......@@ -444,7 +468,21 @@ import '~/notes';
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
expect($tempNote.find('.note-body .note-text').text().trim()).toEqual(sampleComment);
expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment);
});
it('should escape HTML characters from note based on form contents', () => {
const commentWithHtml = '<script>alert("Boom!");</script>';
const $tempNote = this.notes.createPlaceholderNote({
formContent: commentWithHtml,
uniqueId,
isDiscussionNote: false,
currentUsername,
currentUserFullname
});
expect(_.escape).toHaveBeenCalledWith(commentWithHtml);
expect($tempNote.find('.note-body .note-text p').html()).toEqual('&lt;script&gt;alert("Boom!");&lt;/script&gt;');
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
......
......@@ -382,7 +382,6 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column).
with(:users, :new, :integer,
limit: old_column.limit,
null: old_column.null,
precision: old_column.precision,
scale: old_column.scale)
......@@ -391,6 +390,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:update_column_in_batches)
expect(model).to receive(:change_column_null).with(:users, :new, false)
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
......@@ -408,7 +409,6 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column).
with(:users, :new, :integer,
limit: old_column.limit,
null: old_column.null,
precision: old_column.precision,
scale: old_column.scale)
......@@ -417,6 +417,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:update_column_in_batches)
expect(model).to receive(:change_column_null).with(:users, :new, false)
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
......
......@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do
it 'matches issue title endpoint' do
env = build_env(
'/my-group/my-project/issues/123/rendered_title'
'/my-group/my-project/issues/123/realtime_changes'
)
result = described_class.match(env)
......
require 'spec_helper'
describe Gitlab::GitalyClient, lib: true do
describe '.new_channel' do
describe '.stub' do
before { described_class.clear_stubs! }
context 'when passed a UNIX socket address' do
it 'passes the address as-is to GRPC::Core::Channel initializer' do
it 'passes the address as-is to GRPC' do
address = 'unix:/tmp/gitaly.sock'
allow(Gitlab.config.repositories).to receive(:storages).and_return({
'default' => { 'gitaly_address' => address }
})
expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args)
described_class.new_channel(address)
described_class.stub(:commit, 'default')
end
end
......@@ -17,9 +22,13 @@ describe Gitlab::GitalyClient, lib: true do
address = 'localhost:9876'
prefixed_address = "tcp://#{address}"
expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
allow(Gitlab.config.repositories).to receive(:storages).and_return({
'default' => { 'gitaly_address' => prefixed_address }
})
expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args)
described_class.new_channel(prefixed_address)
described_class.stub(:commit, 'default')
end
end
end
......
......@@ -202,7 +202,7 @@ describe Gitlab::Workhorse, lib: true do
context 'when Gitaly is enabled' do
let(:gitaly_params) do
{
GitalyAddress: Gitlab::GitalyClient.get_address('default')
GitalyAddress: Gitlab::GitalyClient.address('default')
}
end
......
......@@ -73,11 +73,11 @@ module FilteredSearchHelpers
end
def remove_recent_searches
execute_script('window.localStorage.removeItem(\'issue-recent-searches\');')
execute_script('window.localStorage.clear();')
end
def set_recent_searches(input)
execute_script("window.localStorage.setItem('issue-recent-searches', '#{input}');")
def set_recent_searches(key, input)
execute_script("window.localStorage.setItem('#{key}', '#{input}');")
end
def wait_for_filtered_search(text)
......
#!/usr/bin/env ruby
#
# # generate-seed-repo-rb
#
# This script generates the seed_repo.rb file used by lib/gitlab/git
# tests. The seed_repo.rb file needs to be updated anytime there is a
# Git push to https://gitlab.com/gitlab-org/gitlab-git-test.
#
# Usage:
#
# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb
#
#
require 'erb'
require 'tempfile'
SOURCE = 'https://gitlab.com/gitlab-org/gitlab-git-test.git'.freeze
SCRIPT_NAME = 'generate-seed-repo-rb'.freeze
REPO_NAME = 'gitlab-git-test.git'.freeze
def main
Dir.mktmpdir do |dir|
unless system(*%W[git clone --bare #{SOURCE} #{REPO_NAME}], chdir: dir)
abort "git clone failed"
end
repo = File.join(dir, REPO_NAME)
erb = ERB.new(DATA.read)
erb.run(binding)
end
end
def capture!(cmd, dir)
output = IO.popen(cmd, 'r', chdir: dir) { |io| io.read }
raise "command failed with #{$?}: #{cmd.join(' ')}" unless $?.success?
output.chomp
end
main
__END__
# This file is generated by <%= SCRIPT_NAME %>. Do not edit this file manually.
#
# Seed repo:
<%= capture!(%w{git log --format=#\ %H\ %s}, repo) %>
module SeedRepo
module BigCommit
ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e".freeze
PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660".freeze
MESSAGE = "Files, encoding and much more".freeze
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
FILES_COUNT = 2
end
module Commit
ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d".freeze
PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9".freeze
MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n".freeze
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"].freeze
FILES_COUNT = 2
C_FILE_PATH = "files/ruby".freeze
C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze
BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}.freeze
BLOB_FILE_PATH = "app/views/keys/show.html.haml".freeze
end
module EmptyCommit
ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9".freeze
PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
MESSAGE = "Empty commit".freeze
AUTHOR_FULL_NAME = "Rémy Coutable".freeze
FILES = [].freeze
FILES_COUNT = FILES.count
end
module EncodingCommit
ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8".freeze
MESSAGE = "Add ISO-8859-encoded file".freeze
AUTHOR_FULL_NAME = "Stan Hu".freeze
FILES = ["encoding/iso8859.txt"].freeze
FILES_COUNT = FILES.count
end
module FirstCommit
ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863".freeze
PARENT_ID = nil
MESSAGE = "Initial commit".freeze
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
FILES = ["LICENSE", ".gitignore", "README.md"].freeze
FILES_COUNT = 3
end
module LastCommit
ID = <%= capture!(%w[git show -s --format=%H HEAD], repo).inspect %>.freeze
PARENT_ID = <%= capture!(%w[git show -s --format=%P HEAD], repo).split.last.inspect %>.freeze
MESSAGE = <%= capture!(%w[git show -s --format=%s HEAD], repo).inspect %>.freeze
AUTHOR_FULL_NAME = <%= capture!(%w[git show -s --format=%an HEAD], repo).inspect %>.freeze
FILES = <%=
parents = capture!(%w[git show -s --format=%P HEAD], repo).split
merge_base = parents.size > 1 ? capture!(%w[git merge-base] + parents, repo) : parents.first
capture!( %W[git diff --name-only #{merge_base}..HEAD --], repo).split("\n").inspect
%>.freeze
FILES_COUNT = FILES.count
end
module Repo
HEAD = "master".freeze
BRANCHES = %w[
<%= capture!(%W[git for-each-ref --format=#{' ' * 3}%(refname:strip=2) refs/heads/], repo) %>
].freeze
TAGS = %w[
<%= capture!(%W[git for-each-ref --format=#{' ' * 3}%(refname:strip=2) refs/tags/], repo) %>
].freeze
end
module RubyBlob
ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze
NAME = "popen.rb".freeze
CONTENT = <<-eos.freeze
require 'fileutils'
require 'open3'
module Popen
extend self
def popen(cmd, path=nil)
unless cmd.is_a?(Array)
raise RuntimeError, "System commands must be given as an array of strings"
end
path ||= Dir.pwd
vars = {
"PWD" => path
}
options = {
chdir: path
}
unless File.directory?(path)
FileUtils.mkdir_p(path)
end
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
end
return @cmd_output, @cmd_status
end
end
eos
end
end
# This file is generated by generate-seed-repo-rb. Do not edit this file manually.
#
# Seed repo:
# 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 Merge branch 'master' into 'master'
# 0e1b353b348f8477bdbec1ef47087171c5032cd9 adds an executable with different permissions
# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
......@@ -94,7 +98,12 @@ module SeedRepo
master
merge-test
].freeze
TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1].freeze
TAGS = %w[
v1.0.0
v1.1.0
v1.2.0
v1.2.1
].freeze
end
module RubyBlob
......
......@@ -120,7 +120,7 @@ module TestEnv
end
def setup_gitaly
socket_path = Gitlab::GitalyClient.get_address('default').sub(/\Aunix:/, '')
socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
......@@ -133,7 +133,8 @@ module TestEnv
def start_gitaly(gitaly_dir)
gitaly_exec = File.join(gitaly_dir, 'gitaly')
gitaly_config = File.join(gitaly_dir, 'config.toml')
@gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => '/dev/null')
log_file = Rails.root.join('log/gitaly-test.log').to_s
@gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => log_file)
end
def stop_gitaly
......
......@@ -236,7 +236,6 @@ describe 'gitlab:app namespace rake task' do
'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
Gitlab::GitalyClient.configure_channels
# Create the projects now, after mocking the settings but before doing the backup
project_a
......
......@@ -4,13 +4,16 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" }
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:project) { create(:project, :repository) }
let(:project_identifier) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
let(:project) do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
end
context "as a sidekiq worker" do
it "reponds to #perform" do
it "responds to #perform" do
expect(described_class.new).to respond_to(:perform)
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