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

[ci skip]
This commit is contained in:
Phil Hughes 2017-05-16 10:37:05 +01:00
commit 86ab9edbb2
79 changed files with 918 additions and 348 deletions

View File

@ -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();

View File

@ -3,6 +3,7 @@ import _ from 'underscore';
class RecentSearchesStore {
constructor(initialState = {}) {
this.state = Object.assign({
isLocalStorageAvailable: true,
recentSearches: [],
}, initialState);
}

View File

@ -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>

View File

@ -1,6 +1,5 @@
<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,
required: true,
},
showForm: {
type: Boolean,
required: true,
},
markdownPreviewUrl: {
descriptionHtml: {
type: String,
required: true,
},
markdownDocs: {
descriptionText: {
type: String,
required: true,
},
updatedAt: {
type: String,
required: false,
default: '',
},
taskStatus: {
type: String,
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-else-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>

View File

@ -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>

View File

@ -1,16 +1,11 @@
<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>

View File

@ -0,0 +1,44 @@
<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>

View File

@ -1,12 +1,8 @@
<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>

View File

@ -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() {

View File

@ -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>

View File

@ -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);

View File

@ -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 {

View File

@ -378,7 +378,7 @@
background-color: $row-hover;
}
.fa-spinner {
.fa-refresh {
font-size: 13px;
margin-left: 3px;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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: {

0
app/controllers/projects/merge_requests_controller.rb Executable file → Normal file
View File

View File

@ -0,0 +1,74 @@
# 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

View File

@ -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/, '')

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
---
title: prevent nav tabs from wrapping to new line
merge_request:
author:

View File

@ -0,0 +1,4 @@
---
title: Remove spinner from loading comment
merge_request:
author:

View File

@ -0,0 +1,4 @@
---
title: Scope issue/merge request recent searches to project
merge_request:
author:

View File

@ -0,0 +1,4 @@
---
title: Allow GitLab instance to start when InfluxDB hostname cannot be resolved
merge_request: 11356
author:

View File

@ -0,0 +1,4 @@
---
title: Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL
merge_request: 11034
author:

View File

@ -0,0 +1,4 @@
---
title: Enable cancelling non-HEAD pending pipelines by default for all projects
merge_request: 11023
author:

View File

@ -0,0 +1,4 @@
---
title: Fix token interpolation when setting the Github remote
merge_request:
author:

View File

@ -0,0 +1,4 @@
---
title: 'Repository browser: handle in-repository submodule urls'
merge_request:
author: David Turner

View File

@ -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

View File

@ -1,6 +1,8 @@
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

View File

@ -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

View File

@ -0,0 +1,13 @@
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

View File

@ -0,0 +1,21 @@
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

View File

@ -0,0 +1,21 @@
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

View File

@ -0,0 +1,15 @@
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

View File

@ -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"

View File

@ -0,0 +1,66 @@
# 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).

View File

@ -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**

View File

@ -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).

View File

@ -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:

View File

@ -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:**

View File

@ -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

View File

@ -1,4 +1,5 @@
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

View File

@ -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

View File

@ -1,8 +1,14 @@
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

View File

@ -1,9 +1,7 @@
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

View File

@ -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(

View File

@ -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'

View File

@ -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

View File

@ -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"
MUTEX = Mutex.new
private_constant :MUTEX
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
unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
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
def self.address(storage)
params = Gitlab.config.repositories.storages[storage]
raise "storage not found: #{storage.inspect}" if params.nil?
address = params['gitaly_address']
unless address.present?
raise "storage #{storage.inspect} is missing a gitaly_address"
end
@channels[storage]
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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,66 @@
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

View File

@ -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')

View File

@ -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;

View File

@ -17,7 +17,7 @@ describe('Title field component', () => {
vm = new Component({
propsData: {
store,
formState: store.formState,
},
}).$mount();
});

View File

@ -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();
});

View File

@ -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', () => {

View File

@ -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)

View File

@ -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)

View File

@ -1,14 +1,19 @@
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 }
})
described_class.new_channel(prefixed_address)
expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args)
described_class.stub(:commit, 'default')
end
end
end

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,162 @@
#!/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

View File

@ -1,4 +1,8 @@
# 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

View File

@ -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

View File

@ -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

View File

@ -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