Commit 86ab9edb authored by Phil Hughes's avatar Phil Hughes

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

[ci skip]
parents 96a46521 5f2b142b
...@@ -16,10 +16,14 @@ class FilteredSearchManager { ...@@ -16,10 +16,14 @@ class FilteredSearchManager {
this.recentSearchesStore = new RecentSearchesStore({ this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(), 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') { if (page === 'merge_requests') {
recentSearchesKey = 'merge-request-recent-searches'; recentSearchesPagePrefix = 'merge-request-recent-searches';
} }
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey); this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
// Fetch recent searches from localStorage // Fetch recent searches from localStorage
...@@ -47,7 +51,7 @@ class FilteredSearchManager { ...@@ -47,7 +51,7 @@ class FilteredSearchManager {
this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore, this.recentSearchesStore,
this.recentSearchesService, this.recentSearchesService,
document.querySelector('.js-filtered-search-history-dropdown'), searchHistoryDropdownElement,
); );
this.recentSearchesRoot.init(); this.recentSearchesRoot.init();
......
...@@ -3,6 +3,7 @@ import _ from 'underscore'; ...@@ -3,6 +3,7 @@ import _ from 'underscore';
class RecentSearchesStore { class RecentSearchesStore {
constructor(initialState = {}) { constructor(initialState = {}) {
this.state = Object.assign({ this.state = Object.assign({
isLocalStorageAvailable: true,
recentSearches: [], recentSearches: [],
}, initialState); }, initialState);
} }
......
...@@ -7,7 +7,7 @@ import Service from '../services/index'; ...@@ -7,7 +7,7 @@ import Service from '../services/index';
import Store from '../stores'; import Store from '../stores';
import titleComponent from './title.vue'; import titleComponent from './title.vue';
import descriptionComponent from './description.vue'; import descriptionComponent from './description.vue';
import editActions from './edit_actions.vue'; import formComponent from './form.vue';
export default { export default {
props: { props: {
...@@ -60,19 +60,18 @@ export default { ...@@ -60,19 +60,18 @@ export default {
return { return {
store, store,
state: store.state, state: store.state,
formState: store.formState,
showForm: false, showForm: false,
}; };
}, },
computed: { computed: {
elementType() { formState() {
return this.showForm ? 'form' : 'div'; return this.store.formState;
}, },
}, },
components: { components: {
descriptionComponent, descriptionComponent,
titleComponent, titleComponent,
editActions, formComponent,
}, },
methods: { methods: {
openForm() { openForm() {
...@@ -82,8 +81,11 @@ export default { ...@@ -82,8 +81,11 @@ export default {
description: this.state.descriptionText, description: this.state.descriptionText,
}; };
}, },
closeForm() {
this.showForm = false;
},
updateIssuable() { updateIssuable() {
this.service.updateIssuable(this.formState) this.service.updateIssuable(this.store.formState)
.then(() => { .then(() => {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
}) })
...@@ -134,32 +136,38 @@ export default { ...@@ -134,32 +136,38 @@ export default {
eventHub.$on('delete.issuable', this.deleteIssuable); eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable); eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm); eventHub.$on('open.form', this.openForm);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable); eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable); 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> </script>
<template> <template>
<div :is="elementType"> <div>
<title-component <form-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
v-if="canUpdate && showForm" 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> </div>
</template> </template>
<script> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import descriptionField from './fields/description.vue';
export default { export default {
mixins: [animateMixin], mixins: [animateMixin],
...@@ -9,45 +8,32 @@ ...@@ -9,45 +8,32 @@
type: Boolean, type: Boolean,
required: true, required: true,
}, },
store: { descriptionHtml: {
type: Object, type: String,
required: true, required: true,
}, },
showForm: { descriptionText: {
type: Boolean, type: String,
required: true, required: true,
}, },
markdownPreviewUrl: { updatedAt: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
markdownDocs: { taskStatus: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { data() {
return { return {
state: this.store.state,
preAnimation: false, preAnimation: false,
pulseAnimation: false, pulseAnimation: false,
timeAgoEl: $('.js-issue-edited-ago'), 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: { watch: {
descriptionHtml() { descriptionHtml() {
this.animateChange(); this.animateChange();
...@@ -91,9 +77,6 @@ ...@@ -91,9 +77,6 @@
} }
}, },
}, },
components: {
descriptionField,
},
mounted() { mounted() {
this.renderGFM(); this.renderGFM();
}, },
...@@ -101,32 +84,25 @@ ...@@ -101,32 +84,25 @@
</script> </script>
<template> <template>
<div :class="{ 'common-note-form': showForm }"> <div
<description-field v-else-if="descriptionHtml"
v-if="showForm" class="description"
:store="store" :class="{
:markdown-preview-url="markdownPreviewUrl" 'js-task-list-container': canUpdate
:markdown-docs="markdownDocs" /> }">
<div <div
v-else-if="descriptionHtml" class="wiki"
class="description"
:class="{ :class="{
'js-task-list-container': canUpdate 'issue-realtime-pre-pulse': preAnimation,
}"> 'issue-realtime-trigger-pulse': pulseAnimation
<div }"
class="wiki" v-html="descriptionHtml"
:class="{ ref="gfm-content">
'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> </div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText">
</textarea>
</div> </div>
</template> </template>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
export default { export default {
props: { props: {
store: { formState: {
type: Object, type: Object,
required: true, required: true,
}, },
...@@ -17,11 +17,6 @@ ...@@ -17,11 +17,6 @@
required: true, required: true,
}, },
}, },
data() {
return {
state: this.store.formState,
};
},
components: { components: {
markdownField, markdownField,
}, },
...@@ -29,7 +24,7 @@ ...@@ -29,7 +24,7 @@
</script> </script>
<template> <template>
<div> <div class="common-note-form">
<label <label
class="sr-only" class="sr-only"
for="issue-description"> for="issue-description">
...@@ -43,7 +38,7 @@ ...@@ -43,7 +38,7 @@
class="note-textarea js-gfm-input js-autosize markdown-area" class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-slash-commands="false" data-supports-slash-commands="false"
aria-label="Description" aria-label="Description"
v-model="state.description" v-model="formState.description"
ref="textatea" ref="textatea"
slot="textarea"> slot="textarea">
</textarea> </textarea>
......
<script> <script>
export default { export default {
props: { props: {
store: { formState: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
data() {
return {
state: this.store.formState,
};
},
}; };
</script> </script>
...@@ -27,6 +22,6 @@ ...@@ -27,6 +22,6 @@
type="text" type="text"
placeholder="Issue title" placeholder="Issue title"
aria-label="Issue title" aria-label="Issue title"
v-model="state.title" /> v-model="formState.title" />
</fieldset> </fieldset>
</template> </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> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import titleField from './fields/title.vue';
export default { export default {
mixins: [animateMixin], mixins: [animateMixin],
components: {
titleField,
},
data() { data() {
return { return {
preAnimation: false, preAnimation: false,
...@@ -27,14 +23,6 @@ ...@@ -27,14 +23,6 @@
type: String, type: String,
required: true, required: true,
}, },
store: {
type: Object,
required: true,
},
showForm: {
type: Boolean,
required: true,
},
}, },
watch: { watch: {
titleHtml() { titleHtml() {
...@@ -53,19 +41,13 @@ ...@@ -53,19 +41,13 @@
</script> </script>
<template> <template>
<div> <h2
<title-field class="title"
v-if="showForm" :class="{
:store="store" /> 'issue-realtime-pre-pulse': preAnimation,
<h2 'issue-realtime-trigger-pulse': pulseAnimation
v-else }"
class="title" v-html="titleHtml"
:class="{ >
'issue-realtime-pre-pulse': preAnimation, </h2>
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
</div>
</template> </template>
...@@ -8,15 +8,15 @@ export default class Service { ...@@ -8,15 +8,15 @@ export default class Service {
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint, {}, { this.resource = Vue.resource(this.endpoint, {}, {
rendered_title: { realtimeChanges: {
method: 'GET', method: 'GET',
url: `${this.endpoint}/rendered_title`, url: `${this.endpoint}/realtime_changes`,
}, },
}); });
} }
getData() { getData() {
return this.resource.rendered_title(); return this.resource.realtimeChanges();
} }
deleteIssuable() { deleteIssuable() {
......
...@@ -24,7 +24,7 @@ const normalizeNewlines = function(str) { ...@@ -24,7 +24,7 @@ const normalizeNewlines = function(str) {
(function() { (function() {
this.Notes = (function() { this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_SLASH_COMMANDS = /^\/\w+/gm; const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm;
Notes.interval = null; Notes.interval = null;
...@@ -1170,6 +1170,7 @@ const normalizeNewlines = function(str) { ...@@ -1170,6 +1170,7 @@ const normalizeNewlines = function(str) {
*/ */
Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) { Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) {
const discussionClass = isDiscussionNote ? 'discussion' : ''; const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $( const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
...@@ -1183,14 +1184,11 @@ const normalizeNewlines = function(str) { ...@@ -1183,14 +1184,11 @@ const normalizeNewlines = function(str) {
<span class="hidden-xs">${currentUserFullname}</span> <span class="hidden-xs">${currentUserFullname}</span>
<span class="note-headline-light">@${currentUsername}</span> <span class="note-headline-light">@${currentUsername}</span>
</a> </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> </div>
<div class="note-body"> <div class="note-body">
<div class="note-text"> <div class="note-text">
<p>${formContent}</p> <p>${escapedFormContent}</p>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -24,10 +24,10 @@ ...@@ -24,10 +24,10 @@
} }
@mixin scrolling-links() { @mixin scrolling-links() {
white-space: nowrap;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
display: flex;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
} }
.nav-links { .nav-links {
display: flex;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
...@@ -42,17 +43,16 @@ ...@@ -42,17 +43,16 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
li { li {
display: inline-block; display: flex;
a { a {
display: inline-block;
padding: $gl-btn-padding; padding: $gl-btn-padding;
padding-bottom: 11px; padding-bottom: 11px;
margin-bottom: -1px;
font-size: 14px; font-size: 14px;
line-height: 28px; line-height: 28px;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
white-space: nowrap;
&:hover, &:hover,
&:active, &:active,
...@@ -85,10 +85,10 @@ ...@@ -85,10 +85,10 @@
.container-fluid { .container-fluid {
background-color: $gray-normal; background-color: $gray-normal;
margin-bottom: 0; margin-bottom: 0;
display: flex;
} }
li { li {
&.active a { &.active a {
border-bottom: none; border-bottom: none;
color: $link-underline-blue; color: $link-underline-blue;
...@@ -137,9 +137,9 @@ ...@@ -137,9 +137,9 @@
} }
.nav-links { .nav-links {
display: inline-block;
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: none;
float: left;
&.wide { &.wide {
width: 100%; width: 100%;
...@@ -337,6 +337,10 @@ ...@@ -337,6 +337,10 @@
border-bottom: none; border-bottom: none;
height: 51px; height: 51px;
@media (min-width: $screen-sm-min) {
justify-content: center;
}
li { li {
a { a {
padding-top: 10px; padding-top: 10px;
...@@ -347,6 +351,7 @@ ...@@ -347,6 +351,7 @@
.scrolling-tabs-container { .scrolling-tabs-container {
position: relative; position: relative;
overflow: hidden;
.nav-links { .nav-links {
@include scrolling-links(); @include scrolling-links();
...@@ -484,10 +489,7 @@ ...@@ -484,10 +489,7 @@
.inner-page-scroll-tabs { .inner-page-scroll-tabs {
position: relative; position: relative;
overflow: hidden;
.nav-links {
padding-bottom: 1px;
}
.fade-right { .fade-right {
@include fade(left, $white-light); @include fade(left, $white-light);
......
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
.right-sidebar-expanded { .right-sidebar-expanded {
padding-right: 0; padding-right: 0;
z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
......
...@@ -378,7 +378,7 @@ ...@@ -378,7 +378,7 @@
background-color: $row-hover; background-color: $row-hover;
} }
.fa-spinner { .fa-refresh {
font-size: 13px; font-size: 13px;
margin-left: 3px; margin-left: 3px;
} }
......
...@@ -23,16 +23,6 @@ ...@@ -23,16 +23,6 @@
.merge-manually { .merge-manually {
@extend .fixed-width-container; @extend .fixed-width-container;
} }
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
}
} }
.merge-request-details { .merge-request-details {
...@@ -206,7 +196,7 @@ ...@@ -206,7 +196,7 @@
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
z-index: 2; z-index: 200;
&.right-sidebar-expanded { &.right-sidebar-expanded {
width: $gutter_width; width: $gutter_width;
......
...@@ -121,6 +121,7 @@ ...@@ -121,6 +121,7 @@
.dropdown-menu { .dropdown-menu {
margin-top: 11px; margin-top: 11px;
z-index: 200;
} }
.ci-action-icon-wrapper { .ci-action-icon-wrapper {
...@@ -690,8 +691,9 @@ ...@@ -690,8 +691,9 @@
.merge-request-tabs-holder { .merge-request-tabs-holder {
top: $header-height; top: $header-height;
z-index: 10; z-index: 100;
background-color: $white-light; background-color: $white-light;
border-bottom: 1px solid $border-color;
@media(min-width: $screen-sm-min) { @media(min-width: $screen-sm-min) {
position: sticky; position: sticky;
...@@ -711,6 +713,16 @@ ...@@ -711,6 +713,16 @@
padding-right: $gl-padding; padding-right: $gl-padding;
} }
} }
.nav-links {
border: 0;
}
}
.merge-request-tabs {
display: flex;
margin-bottom: 0;
padding: 0;
} }
.limit-container-width { .limit-container-width {
...@@ -721,6 +733,15 @@ ...@@ -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) { .limit-container-width:not(.container-limited) {
.merge-request-tabs-holder:not(.affix) { .merge-request-tabs-holder:not(.affix) {
.merge-request-tabs-container { .merge-request-tabs-container {
......
...@@ -609,6 +609,15 @@ ul.notes { ...@@ -609,6 +609,15 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
@media (min-width: $screen-sm-min) {
margin-right: 0;
padding-left: $gl-padding;
}
> div {
white-space: nowrap;
}
.btn-group { .btn-group {
margin-left: -4px; margin-left: -4px;
} }
......
...@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, 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 # 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 # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -199,7 +199,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -199,7 +199,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def rendered_title def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000) Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { 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 ...@@ -7,6 +7,10 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository) def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path) 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/ if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2 namespace, project = $1, $2
project.sub!(/\.git\z/, '') project.sub!(/\.git\z/, '')
......
...@@ -292,7 +292,7 @@ class Issue < ActiveRecord::Base ...@@ -292,7 +292,7 @@ class Issue < ActiveRecord::Base
end end
def expire_etag_cache 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.namespace,
project, project,
self self
......
...@@ -1163,8 +1163,6 @@ class Repository ...@@ -1163,8 +1163,6 @@ class Repository
@project.repository_storage_path @project.repository_storage_path
end end
delegate :gitaly_channel, :gitaly_repository, to: :raw_repository
def initialize_raw_repository def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git') Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end end
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
- else - else
= build.id = build.id
- if build.retried? - 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 :javascript
new Sidebar(); new Sidebar();
...@@ -27,40 +27,42 @@ ...@@ -27,40 +27,42 @@
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true = 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-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs .merge-request-tabs-container
.fade-left= icon('angle-left') .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-right= icon('angle-right') .fade-left= icon('angle-left')
%ul.merge-request-tabs.nav-links.scrolling-tabs .fade-right= icon('angle-right')
%li.notes-tab .nav-links.scrolling-tabs
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do %ul.merge-request-tabs
Discussion %li.notes-tab
%span.badge= @merge_request.related_notes.user.count = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
- if @merge_request.source_project Discussion
%li.commits-tab %span.badge= @merge_request.related_notes.user.count
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - if @merge_request.source_project
Commits %li.commits-tab
%span.badge= @commits_count = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
- if @pipelines.any? Commits
%li.pipelines-tab %span.badge= @commits_count
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - if @pipelines.any?
Pipelines %li.pipelines-tab
%span.badge= @pipelines.size = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
%li.diffs-tab Pipelines
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do %span.badge= @pipelines.size
Changes %li.diffs-tab
%span.badge= @merge_request.diff_size = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
%li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } Changes
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } %span.badge= @merge_request.diff_size
%div #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
.line-resolve-all{ "v-show" => "discussionCount > 0", %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } %div
%span.line-resolve-btn.is-disabled{ type: "button", .line-resolve-all{ "v-show" => "discussionCount > 0",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
= render "shared/icons/icon_status_success.svg" %span.line-resolve-btn.is-disabled{ type: "button",
%span.line-resolve-text ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved = render "shared/icons/icon_status_success.svg"
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request %span.line-resolve-text
= render "discussions/jump_to_next" {{ 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 .tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
dropdown_class: "filtered-search-history-dropdown", dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content", content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do 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 .filtered-search-box-input-container
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %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 ...@@ -16,7 +16,7 @@ Rails.application.configure do
# config.assets.css_compressor = :sass # config.assets.css_compressor = :sass
# Don't fallback to assets pipeline if a precompiled asset is missed # 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 # Generate digests for assets URLs
config.assets.digest = true config.assets.digest = true
......
require 'uri' require 'uri'
# Make sure we initialize our Gitaly channels before Sidekiq starts multi-threaded execution.
if Gitlab.config.gitaly.enabled || Rails.env.test? 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 end
...@@ -244,7 +244,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -244,7 +244,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests get :referenced_merge_requests
get :related_branches get :related_branches
get :can_create_branch get :can_create_branch
get :rendered_title get :realtime_changes
post :create_merge_request post :create_merge_request
end end
collection do 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 ...@@ -235,6 +235,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do
t.boolean "retried" t.boolean "retried"
end 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", "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", "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 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 ...@@ -284,6 +285,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do
t.integer "pipeline_schedule_id" t.integer "pipeline_schedule_id"
end 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", ["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", "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 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 ...@@ -986,7 +988,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do
t.text "description_html" t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.boolean "printing_merge_request_link_enabled", default: true, null: false 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.string "import_jid"
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.datetime "last_repository_updated_at" 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 ...@@ -12,6 +12,10 @@ They are written by members of the GitLab Team and by
- **LDAP** - **LDAP**
- [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md) - [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
- **GitLab Pages from A to Z** - **GitLab Pages from A to Z**
......
...@@ -155,7 +155,7 @@ Find more information about different Runners in the ...@@ -155,7 +155,7 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation. [Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to 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 official Runner supported by GitLab is written in Go and its documentation
can be found at <https://docs.gitlab.com/runner/>. 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 ...@@ -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. described in the next section.
Once the Runner has been set up, you should see it on the Runners page of your 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) ![Activated runners](img/runners_activated.png)
...@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can ...@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can
build any project. build any project.
To enable the **Shared Runners** you have to go to your project's 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). [Read more on Shared Runners](../runners/README.md).
......
...@@ -392,7 +392,7 @@ Once you [have configured](#configuration) GitLab in your `values.yml` file, ...@@ -392,7 +392,7 @@ Once you [have configured](#configuration) GitLab in your `values.yml` file,
run the following: run the following:
```bash ```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: where:
...@@ -407,7 +407,7 @@ Once your GitLab Chart is installed, configuration changes and chart updates ...@@ -407,7 +407,7 @@ Once your GitLab Chart is installed, configuration changes and chart updates
should we done using `helm upgrade` should we done using `helm upgrade`
```bash ```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: where:
......
...@@ -22,6 +22,7 @@ We've gathered some resources to help you to get the best from Git with GitLab. ...@@ -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) - [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) - [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **Articles:** - **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/) - [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/) - [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:** - **Presentations:**
......
...@@ -56,16 +56,7 @@ module API ...@@ -56,16 +56,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
users = User.all users = UsersFinder.new(current_user, params).execute
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
entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic
present paginate(users), with: entity present paginate(users), with: entity
......
require_relative 'error' require_relative 'error'
module Github module Github
class Import class Import
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
...@@ -6,6 +7,7 @@ module Github ...@@ -6,6 +7,7 @@ module Github
class MergeRequest < ::MergeRequest class MergeRequest < ::MergeRequest
self.table_name = 'merge_requests' self.table_name = 'merge_requests'
self.reset_callbacks :create
self.reset_callbacks :save self.reset_callbacks :save
self.reset_callbacks :commit self.reset_callbacks :commit
self.reset_callbacks :update self.reset_callbacks :update
...@@ -16,6 +18,7 @@ module Github ...@@ -16,6 +18,7 @@ module Github
self.table_name = 'issues' self.table_name = 'issues'
self.reset_callbacks :save self.reset_callbacks :save
self.reset_callbacks :create
self.reset_callbacks :commit self.reset_callbacks :commit
self.reset_callbacks :update self.reset_callbacks :update
self.reset_callbacks :validate self.reset_callbacks :validate
...@@ -79,7 +82,7 @@ module Github ...@@ -79,7 +82,7 @@ module Github
def fetch_repository def fetch_repository
begin begin
project.create_repository unless project.repository.exists? 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.set_remote_as_mirror('github')
project.repository.fetch_remote('github', forced: true) project.repository.fetch_remote('github', forced: true)
rescue Gitlab::Shell::Error => e rescue Gitlab::Shell::Error => e
......
...@@ -283,7 +283,6 @@ module Gitlab ...@@ -283,7 +283,6 @@ module Gitlab
add_column(table, new, new_type, add_column(table, new, new_type,
limit: old_col.limit, limit: old_col.limit,
null: old_col.null,
precision: old_col.precision, precision: old_col.precision,
scale: old_col.scale) scale: old_col.scale)
...@@ -307,6 +306,8 @@ module Gitlab ...@@ -307,6 +306,8 @@ module Gitlab
update_column_in_batches(table, new, Arel::Table.new(table)[old]) 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_indexes(table, old, new)
copy_foreign_keys(table, old, new) copy_foreign_keys(table, old, new)
end end
......
module Gitlab module Gitlab
module DependencyLinker module DependencyLinker
class BaseLinker class BaseLinker
def self.link(plain_text, highlighted_text) class_attribute :file_type
new(plain_text, highlighted_text).link
def self.support?(blob_name)
Gitlab::FileDetector.type_of(blob_name) == file_type
end
def self.link(*args)
new(*args).link
end end
attr_accessor :plain_text, :highlighted_text attr_accessor :plain_text, :highlighted_text
......
module Gitlab module Gitlab
module DependencyLinker module DependencyLinker
class GemfileLinker < BaseLinker class GemfileLinker < BaseLinker
def self.support?(blob_name) self.file_type = :gemfile
blob_name == 'Gemfile' || blob_name == 'gems.rb'
end
private private
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
# - Don't contain a reserved word (expect for the words used in the # - Don't contain a reserved word (expect for the words used in the
# regex itself) # regex itself)
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route # - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/rendered_title` for the `issue_title` route # - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues rendered_title USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze commit pipelines merge_requests new].freeze
RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS) RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
'issue_notes' 'issue_notes'
), ),
Gitlab::EtagCaching::Router::Route.new( 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' 'issue_title'
), ),
Gitlab::EtagCaching::Router::Route.new( Gitlab::EtagCaching::Router::Route.new(
......
...@@ -12,6 +12,7 @@ module Gitlab ...@@ -12,6 +12,7 @@ module Gitlab
version: 'version', version: 'version',
gitignore: '.gitignore', gitignore: '.gitignore',
koding: '.koding.yml', koding: '.koding.yml',
gemfile: /\A(Gemfile|gems\.rb)\z/,
gitlab_ci: '.gitlab-ci.yml', gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/, avatar: /\Alogo\.(png|jpg|gif)\z/,
route_map: 'route-map.yml' route_map: 'route-map.yml'
......
...@@ -27,13 +27,15 @@ module Gitlab ...@@ -27,13 +27,15 @@ module Gitlab
# Rugged repo object # Rugged repo object
attr_reader :rugged attr_reader :rugged
attr_reader :storage
# 'path' must be the path to a _bare_ git repository, e.g. # 'path' must be the path to a _bare_ git repository, e.g.
# /path/to/my-repo.git # /path/to/my-repo.git
def initialize(repository_storage, relative_path) def initialize(storage, relative_path)
@repository_storage = repository_storage @storage = storage
@relative_path = relative_path @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) @path = File.join(storage_path, @relative_path)
@name = @relative_path.split("/").last @name = @relative_path.split("/").last
@attributes = Gitlab::Git::Attributes.new(path) @attributes = Gitlab::Git::Attributes.new(path)
...@@ -114,7 +116,7 @@ module Gitlab ...@@ -114,7 +116,7 @@ module Gitlab
# Returns the number of valid branches # Returns the number of valid branches
def branch_count def branch_count
Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled| gitaly_migrate(:branch_names) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.count_branch_names gitaly_ref_client.count_branch_names
else else
...@@ -133,7 +135,7 @@ module Gitlab ...@@ -133,7 +135,7 @@ module Gitlab
# Returns the number of valid tags # Returns the number of valid tags
def tag_count def tag_count
Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled| gitaly_migrate(:tag_names) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.count_tag_names gitaly_ref_client.count_tag_names
else else
...@@ -471,7 +473,7 @@ module Gitlab ...@@ -471,7 +473,7 @@ module Gitlab
def ref_name_for_sha(ref_path, sha) def ref_name_for_sha(ref_path, sha)
# NOTE: This feature is intentionally disabled until # NOTE: This feature is intentionally disabled until
# https://gitlab.com/gitlab-org/gitaly/issues/180 is resolved # 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 # if is_enabled
# gitaly_ref_client.find_ref_name(sha, ref_path) # gitaly_ref_client.find_ref_name(sha, ref_path)
# else # else
...@@ -965,11 +967,7 @@ module Gitlab ...@@ -965,11 +967,7 @@ module Gitlab
end end
def gitaly_repository def gitaly_repository
Gitlab::GitalyClient::Util.repository(@repository_storage, @relative_path) Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
end
def gitaly_channel
Gitlab::GitalyClient.get_channel(@repository_storage)
end end
private private
......
...@@ -4,56 +4,42 @@ module Gitlab ...@@ -4,56 +4,42 @@ module Gitlab
module GitalyClient module GitalyClient
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
# This function is not thread-safe because it updates Hashes in instance variables. MUTEX = Mutex.new
def self.configure_channels private_constant :MUTEX
@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
unless URI(address).scheme.in?(%w(tcp unix)) def self.stub(name, storage)
raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'" 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 end
@addresses[name] = address
@channels[name] = new_channel(address)
end end
end end
def self.new_channel(address) def self.clear_stubs!
address = address.sub(%r{^tcp://}, '') if URI(address).scheme == 'tcp' MUTEX.synchronize do
# NOTE: When Gitaly runs on a Unix socket, permissions are @stubs = nil
# handled using the file system and no additional authentication is end
# 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)
end end
def self.get_channel(storage) def self.address(storage)
if !Rails.env.production? && @channels.nil? params = Gitlab.config.repositories.storages[storage]
# In development mode the Rails auto-loader may reset the instance raise "storage not found: #{storage.inspect}" if params.nil?
# 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
@channels[storage] address = params['gitaly_address']
end unless address.present?
raise "storage #{storage.inspect} is missing a gitaly_address"
end
def self.get_address(storage) unless URI(address).scheme.in?(%w(tcp unix))
if !Rails.env.production? && @addresses.nil? raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
# 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
end end
@addresses[storage] address
end end
def self.enabled? def self.enabled?
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def is_ancestor(ancestor_id, child_id) 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( request = Gitaly::CommitIsAncestorRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
ancestor_id: ancestor_id, ancestor_id: ancestor_id,
...@@ -52,7 +52,7 @@ module Gitlab ...@@ -52,7 +52,7 @@ module Gitlab
end end
def diff_service_stub def diff_service_stub
Gitaly::Diff::Stub.new(nil, nil, channel_override: @repository.gitaly_channel) GitalyClient.stub(:diff, @repository.storage)
end end
end end
end end
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
# 'repository' is a Gitlab::Git::Repository # 'repository' is a Gitlab::Git::Repository
def initialize(repository) def initialize(repository)
@gitaly_repo = repository.gitaly_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 end
def post_receive def post_receive
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
# 'repository' is a Gitlab::Git::Repository # 'repository' is a Gitlab::Git::Repository
def initialize(repository) def initialize(repository)
@gitaly_repo = repository.gitaly_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 end
def default_branch_name def default_branch_name
......
...@@ -49,6 +49,9 @@ module Gitlab ...@@ -49,6 +49,9 @@ module Gitlab
end end
end 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 end
def self.prepare_metrics(metrics) def self.prepare_metrics(metrics)
......
...@@ -26,7 +26,7 @@ module Gitlab ...@@ -26,7 +26,7 @@ module Gitlab
} }
if Gitlab.config.gitaly.enabled 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 params[:Repository] = repository.gitaly_repository.to_h
feature_enabled = case action.to_s feature_enabled = case action.to_s
......
...@@ -5,14 +5,7 @@ describe 'Auto deploy' do ...@@ -5,14 +5,7 @@ describe 'Auto deploy' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
before do before do
project.create_kubernetes_service( create :kubernetes_service, project: project
active: true,
properties: {
namespace: project.path,
api_url: 'https://kubernetes.example.com',
token: 'a' * 40
}
)
project.team << [user, :master] project.team << [user, :master]
login_as user login_as user
end end
......
...@@ -14,7 +14,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu ...@@ -14,7 +14,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end end
it 'shows a button to resolve all discussions by creating a new issue' do 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) 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
end end
......
...@@ -3,17 +3,17 @@ require 'spec_helper' ...@@ -3,17 +3,17 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers include FilteredSearchHelpers
let!(:group) { create(:group) } let(:project_1) { create(:empty_project, :public) }
let!(:project) { create(:project, group: group) } let(:project_2) { create(:empty_project, :public) }
let!(:user) { create(:user) } let(:project_1_local_storage_key) { "#{project_1.full_path}-issue-recent-searches" }
before do before do
Capybara.ignore_hidden_elements = false Capybara.ignore_hidden_elements = false
project.add_master(user) create(:issue, project: project_1)
group.add_developer(user) create(:issue, project: project_2)
create(:issue, project: project)
login_as(user)
# Visit any fast-loading page so we can clear local storage without a DOM exception
visit '/404'
remove_recent_searches remove_recent_searches
end end
...@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do ...@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'searching adds to recent searches' do 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('foo', submit: true)
input_filtered_search('bar', submit: true) input_filtered_search('bar', submit: true)
...@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do ...@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'visiting URL with search params adds to recent searches' do 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_1.namespace, project_1, 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: 'qux', search: 'garply')
items = all('.filtered-search-history-dropdown-item', visible: false) items = all('.filtered-search-history-dropdown-item', visible: false)
...@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do ...@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'saved recent searches are restored last on the list' do 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) items = all('.filtered-search-history-dropdown-item', visible: false)
...@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do ...@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do
expect(items[2].text).to eq('saved2') expect(items[2].text).to eq('saved2')
end 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 it 'clicking item fills search input' do
set_recent_searches('["foo", "bar"]') set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click') all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
wait_for_filtered_search('foo') wait_for_filtered_search('foo')
...@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do ...@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'clear recent searches button, clears recent searches' do it 'clear recent searches button, clears recent searches' do
set_recent_searches('["foo"]') set_recent_searches(project_1_local_storage_key, '["foo"]')
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
items_before = all('.filtered-search-history-dropdown-item', visible: false) items_before = all('.filtered-search-history-dropdown-item', visible: false)
...@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do ...@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'shows flash error when failed to parse saved history' do it 'shows flash error when failed to parse saved history' do
set_recent_searches('fail') set_recent_searches(project_1_local_storage_key, 'fail')
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches') expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches')
end 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 ...@@ -81,6 +81,19 @@ describe SubmoduleHelper do
end end
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 context 'submodule on gitlab.com' do
it 'detects ssh' do it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git') stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
......
...@@ -29,7 +29,7 @@ describe('Issuable output', () => { ...@@ -29,7 +29,7 @@ describe('Issuable output', () => {
propsData: { propsData: {
canUpdate: true, canUpdate: true,
canDestroy: true, canDestroy: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title', endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1', issuableRef: '#1',
initialTitle: '', initialTitle: '',
initialDescriptionHtml: '', initialDescriptionHtml: '',
...@@ -75,18 +75,6 @@ describe('Issuable output', () => { ...@@ -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) => { it('does not show actions if permissions are incorrect', (done) => {
vm.showForm = true; vm.showForm = true;
vm.canUpdate = false; vm.canUpdate = false;
......
...@@ -17,7 +17,7 @@ describe('Title field component', () => { ...@@ -17,7 +17,7 @@ describe('Title field component', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
store, formState: store.formState,
}, },
}).$mount(); }).$mount();
}); });
......
...@@ -7,17 +7,18 @@ describe('Title component', () => { ...@@ -7,17 +7,18 @@ describe('Title component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(titleComponent); const Component = Vue.extend(titleComponent);
const store = new Store({
titleHtml: '',
descriptionHtml: '',
issuableRef: '',
});
vm = new Component({ vm = new Component({
propsData: { propsData: {
issuableRef: '#1', issuableRef: '#1',
titleHtml: 'Testing <img />', titleHtml: 'Testing <img />',
titleText: 'Testing', titleText: 'Testing',
showForm: false, showForm: false,
store: new Store({ formState: store.formState,
titleHtml: '',
descriptionHtml: '',
issuableRef: '',
}),
}, },
}).$mount(); }).$mount();
}); });
......
...@@ -377,7 +377,7 @@ import '~/notes'; ...@@ -377,7 +377,7 @@ import '~/notes';
}); });
it('should return true when comment begins with a slash command', () => { 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); const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeTruthy(); expect(hasSlashCommands).toBeTruthy();
...@@ -401,10 +401,18 @@ import '~/notes'; ...@@ -401,10 +401,18 @@ import '~/notes';
describe('stripSlashCommands', () => { describe('stripSlashCommands', () => {
it('should strip slash commands from the comment which begins with a slash command', () => { it('should strip slash commands from the comment which begins with a slash command', () => {
this.notes = new Notes(); 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); 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', () => { it('should NOT strip string that has slashes within', () => {
...@@ -424,6 +432,22 @@ import '~/notes'; ...@@ -424,6 +432,22 @@ import '~/notes';
beforeEach(() => { beforeEach(() => {
this.notes = new Notes('', []); 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', () => { it('should return constructed placeholder element for regular note based on form contents', () => {
...@@ -444,7 +468,21 @@ import '~/notes'; ...@@ -444,7 +468,21 @@ import '~/notes';
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname); expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`); 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', () => { it('should return constructed placeholder element for discussion note based on form contents', () => {
......
...@@ -382,7 +382,6 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -382,7 +382,6 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column). expect(model).to receive(:add_column).
with(:users, :new, :integer, with(:users, :new, :integer,
limit: old_column.limit, limit: old_column.limit,
null: old_column.null,
precision: old_column.precision, precision: old_column.precision,
scale: old_column.scale) scale: old_column.scale)
...@@ -391,6 +390,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -391,6 +390,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:update_column_in_batches) 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_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).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 ...@@ -408,7 +409,6 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column). expect(model).to receive(:add_column).
with(:users, :new, :integer, with(:users, :new, :integer,
limit: old_column.limit, limit: old_column.limit,
null: old_column.null,
precision: old_column.precision, precision: old_column.precision,
scale: old_column.scale) scale: old_column.scale)
...@@ -417,6 +417,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -417,6 +417,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:update_column_in_batches) 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_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new) expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
......
...@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do ...@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do
it 'matches issue title endpoint' do it 'matches issue title endpoint' do
env = build_env( env = build_env(
'/my-group/my-project/issues/123/rendered_title' '/my-group/my-project/issues/123/realtime_changes'
) )
result = described_class.match(env) result = described_class.match(env)
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GitalyClient, lib: true do 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 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' 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
end end
...@@ -17,9 +22,13 @@ describe Gitlab::GitalyClient, lib: true do ...@@ -17,9 +22,13 @@ describe Gitlab::GitalyClient, lib: true do
address = 'localhost:9876' address = 'localhost:9876'
prefixed_address = "tcp://#{address}" 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 end
end end
......
...@@ -202,7 +202,7 @@ describe Gitlab::Workhorse, lib: true do ...@@ -202,7 +202,7 @@ describe Gitlab::Workhorse, lib: true do
context 'when Gitaly is enabled' do context 'when Gitaly is enabled' do
let(:gitaly_params) do let(:gitaly_params) do
{ {
GitalyAddress: Gitlab::GitalyClient.get_address('default') GitalyAddress: Gitlab::GitalyClient.address('default')
} }
end end
......
...@@ -73,11 +73,11 @@ module FilteredSearchHelpers ...@@ -73,11 +73,11 @@ module FilteredSearchHelpers
end end
def remove_recent_searches def remove_recent_searches
execute_script('window.localStorage.removeItem(\'issue-recent-searches\');') execute_script('window.localStorage.clear();')
end end
def set_recent_searches(input) def set_recent_searches(key, input)
execute_script("window.localStorage.setItem('issue-recent-searches', '#{input}');") execute_script("window.localStorage.setItem('#{key}', '#{input}');")
end end
def wait_for_filtered_search(text) 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: # Seed repo:
# 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 Merge branch 'master' into 'master'
# 0e1b353b348f8477bdbec1ef47087171c5032cd9 adds an executable with different permissions
# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master' # 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers # 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file # 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
...@@ -94,7 +98,12 @@ module SeedRepo ...@@ -94,7 +98,12 @@ module SeedRepo
master master
merge-test merge-test
].freeze ].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 end
module RubyBlob module RubyBlob
......
...@@ -120,7 +120,7 @@ module TestEnv ...@@ -120,7 +120,7 @@ module TestEnv
end end
def setup_gitaly 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) gitaly_dir = File.dirname(socket_path)
unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
...@@ -133,7 +133,8 @@ module TestEnv ...@@ -133,7 +133,8 @@ module TestEnv
def start_gitaly(gitaly_dir) def start_gitaly(gitaly_dir)
gitaly_exec = File.join(gitaly_dir, 'gitaly') gitaly_exec = File.join(gitaly_dir, 'gitaly')
gitaly_config = File.join(gitaly_dir, 'config.toml') 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 end
def stop_gitaly def stop_gitaly
......
...@@ -236,7 +236,6 @@ describe 'gitlab:app namespace rake task' do ...@@ -236,7 +236,6 @@ describe 'gitlab:app namespace rake task' do
'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address } 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
} }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) 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 # Create the projects now, after mocking the settings but before doing the backup
project_a project_a
......
...@@ -4,13 +4,16 @@ describe PostReceive do ...@@ -4,13 +4,16 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } 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(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:project) { create(:project, :repository) }
let(:project_identifier) { "project-#{project.id}" } let(:project_identifier) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) } let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id } let(:key_id) { key.shell_id }
let(:project) do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
end
context "as a sidekiq worker" do context "as a sidekiq worker" do
it "reponds to #perform" do it "responds to #perform" do
expect(described_class.new).to respond_to(:perform) expect(described_class.new).to respond_to(:perform)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment