Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
fb462e2e
Commit
fb462e2e
authored
May 17, 2017
by
Rémy Coutable
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-05-10
Signed-off-by:
Rémy Coutable
<
remy@rymai.me
>
parents
47149736
2b778174
Changes
52
Hide whitespace changes
Inline
Side-by-side
Showing
52 changed files
with
659 additions
and
456 deletions
+659
-456
.gitlab-ci.yml
.gitlab-ci.yml
+2
-2
app/assets/javascripts/diff_notes/components/diff_note_avatars.js
...ts/javascripts/diff_notes/components/diff_note_avatars.js
+1
-1
app/assets/javascripts/dispatcher.js
app/assets/javascripts/dispatcher.js
+3
-0
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+7
-3
app/assets/javascripts/filtered_search/stores/recent_searches_store.js
...vascripts/filtered_search/stores/recent_searches_store.js
+1
-0
app/assets/javascripts/gfm_auto_complete.js
app/assets/javascripts/gfm_auto_complete.js
+248
-234
app/assets/javascripts/gl_form.js
app/assets/javascripts/gl_form.js
+3
-1
app/assets/javascripts/issuable_form.js
app/assets/javascripts/issuable_form.js
+3
-1
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+0
-1
app/assets/javascripts/merge_request_tabs.js
app/assets/javascripts/merge_request_tabs.js
+17
-1
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+44
-32
app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
...vue_merge_request_widget/components/mr_widget_pipeline.js
+1
-1
app/assets/stylesheets/framework/timeline.scss
app/assets/stylesheets/framework/timeline.scss
+32
-31
app/assets/stylesheets/pages/merge_requests.scss
app/assets/stylesheets/pages/merge_requests.scss
+4
-0
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+3
-3
app/models/ee/user.rb
app/models/ee/user.rb
+10
-0
app/models/issue.rb
app/models/issue.rb
+1
-1
app/services/geo/repository_sync_service.rb
app/services/geo/repository_sync_service.rb
+3
-3
app/views/layouts/_init_auto_complete.html.haml
app/views/layouts/_init_auto_complete.html.haml
+1
-2
app/views/layouts/application.html.haml
app/views/layouts/application.html.haml
+1
-1
app/views/projects/boards/_show.html.haml
app/views/projects/boards/_show.html.haml
+3
-3
app/views/projects/issues/index.html.haml
app/views/projects/issues/index.html.haml
+3
-2
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+1
-1
app/views/projects/merge_requests/_show.html.haml
app/views/projects/merge_requests/_show.html.haml
+2
-1
app/views/projects/merge_requests/index.html.haml
app/views/projects/merge_requests/index.html.haml
+2
-1
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+1
-1
app/workers/geo_repository_sync_worker.rb
app/workers/geo_repository_sync_worker.rb
+9
-9
changelogs/unreleased-ee/2366-geo-sign-out-broken.yml
changelogs/unreleased-ee/2366-geo-sign-out-broken.yml
+4
-0
changelogs/unreleased/31902-namespace-recent-searches-to-project.yml
...unreleased/31902-namespace-recent-searches-to-project.yml
+4
-0
config/gitlab.yml.example
config/gitlab.yml.example
+4
-4
config/initializers/1_settings.rb
config/initializers/1_settings.rb
+6
-6
config/routes/project.rb
config/routes/project.rb
+1
-1
config/webpack.config.js
config/webpack.config.js
+9
-1
doc/development/writing_documentation.md
doc/development/writing_documentation.md
+13
-6
doc/user/project/merge_requests/merge_request_approvals.md
doc/user/project/merge_requests/merge_request_approvals.md
+9
-2
lib/gitlab/etag_caching/router.rb
lib/gitlab/etag_caching/router.rb
+3
-3
lib/gitlab/geo.rb
lib/gitlab/geo.rb
+5
-5
spec/features/issues/filtered_search/recent_searches_spec.rb
spec/features/issues/filtered_search/recent_searches_spec.rb
+36
-18
spec/features/projects/gfm_autocomplete_load_spec.rb
spec/features/projects/gfm_autocomplete_load_spec.rb
+1
-1
spec/javascripts/gfm_auto_complete_spec.js
spec/javascripts/gfm_auto_complete_spec.js
+11
-9
spec/javascripts/issue_show/issue_title_description_spec.js
spec/javascripts/issue_show/issue_title_description_spec.js
+1
-1
spec/javascripts/notes_spec.js
spec/javascripts/notes_spec.js
+42
-4
spec/lib/gitlab/etag_caching/router_spec.rb
spec/lib/gitlab/etag_caching/router_spec.rb
+1
-1
spec/lib/gitlab/geo_spec.rb
spec/lib/gitlab/geo_spec.rb
+4
-7
spec/models/user_spec.rb
spec/models/user_spec.rb
+32
-0
spec/services/ci/create_pipeline_service_spec.rb
spec/services/ci/create_pipeline_service_spec.rb
+8
-6
spec/services/geo/repository_sync_service_spec.rb
spec/services/geo/repository_sync_service_spec.rb
+3
-3
spec/services/issues/close_service_spec.rb
spec/services/issues/close_service_spec.rb
+9
-5
spec/services/merge_requests/create_service_spec.rb
spec/services/merge_requests/create_service_spec.rb
+6
-4
spec/services/merge_requests/update_service_spec.rb
spec/services/merge_requests/update_service_spec.rb
+23
-15
spec/support/filtered_search_helpers.rb
spec/support/filtered_search_helpers.rb
+3
-3
spec/workers/geo/geo_repository_sync_worker_spec.rb
spec/workers/geo/geo_repository_sync_worker_spec.rb
+15
-15
No files found.
.gitlab-ci.yml
View file @
fb462e2e
...
...
@@ -79,7 +79,7 @@ stages:
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
.except-docs
:
&except-docs
except
:
-
/
^docs\/.*
/
-
/
(^docs[\/-].*|.*-docs$)
/
.rspec-knapsack
:
&rspec-knapsack
stage
:
test
...
...
@@ -313,7 +313,7 @@ downtime_check:
-
master
-
tags
-
/^[\d-]+-stable(-ee)?$/
-
/
^docs\/*
/
-
/
(^docs[\/-].*|.*-docs$)
/
.db-migrate-reset
:
&db-migrate-reset
stage
:
test
...
...
app/assets/javascripts/diff_notes/components/diff_note_avatars.js
View file @
fb462e2e
...
...
@@ -120,7 +120,7 @@ const DiffNoteAvatars = Vue.extend({
},
methods
:
{
clickedAvatar
(
e
)
{
notes
.
a
ddDiffNote
(
e
);
notes
.
onA
ddDiffNote
(
e
);
// Toggle the active state of the toggle all button
this
.
toggleDiscussionsToggleState
();
...
...
app/assets/javascripts/dispatcher.js
View file @
fb462e2e
...
...
@@ -57,6 +57,7 @@ import BlobViewer from './blob/viewer/index';
import
GeoNodes
from
'
./geo_nodes
'
;
import
ServiceDeskRoot
from
'
./projects/settings_service_desk/service_desk_root
'
;
import
AutoWidthDropdownSelect
from
'
./issuable/auto_width_dropdown_select
'
;
import
GfmAutoComplete
from
'
./gfm_auto_complete
'
;
const
ShortcutsBlob
=
require
(
'
./shortcuts_blob
'
);
...
...
@@ -83,6 +84,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path
=
page
.
split
(
'
:
'
);
shortcut_handler
=
null
;
new
GfmAutoComplete
(
gl
.
GfmAutoComplete
&&
gl
.
GfmAutoComplete
.
dataSources
).
setup
();
function
initBlob
()
{
new
LineHighlighter
();
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
fb462e2e
...
...
@@ -20,10 +20,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
'
)
{
recentSearches
Key
=
'
merge-request-recent-searches
'
;
recentSearches
PagePrefix
=
'
merge-request-recent-searches
'
;
}
const
recentSearchesKey
=
`
${
projectPath
}
-
${
recentSearchesPagePrefix
}
`
;
this
.
recentSearchesService
=
new
RecentSearchesService
(
recentSearchesKey
);
// Fetch recent searches from localStorage
...
...
@@ -51,7 +55,7 @@ class FilteredSearchManager {
this
.
recentSearchesRoot
=
new
RecentSearchesRoot
(
this
.
recentSearchesStore
,
this
.
recentSearchesService
,
document
.
querySelector
(
'
.js-filtered-search-history-dropdown
'
)
,
searchHistoryDropdownElement
,
);
this
.
recentSearchesRoot
.
init
();
...
...
app/assets/javascripts/filtered_search/stores/recent_searches_store.js
View file @
fb462e2e
...
...
@@ -3,6 +3,7 @@ import _ from 'underscore';
class
RecentSearchesStore
{
constructor
(
initialState
=
{})
{
this
.
state
=
Object
.
assign
({
isLocalStorageAvailable
:
true
,
recentSearches
:
[],
},
initialState
);
}
...
...
app/assets/javascripts/gfm_auto_complete.js
View file @
fb462e2e
/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
import
emojiMap
from
'
emojis/digests.json
'
;
import
emojiAliases
from
'
emojis/aliases.json
'
;
import
{
glEmojiTag
}
from
'
~/behaviors/gl_emoji
'
;
import
glRegexp
from
'
~/lib/utils/regexp
'
;
// Creates the variables for setting up GFM auto-completion
window
.
gl
=
window
.
gl
||
{};
function
sanitize
(
str
)
{
return
str
.
replace
(
/<
(?:
.|
\n)
*
?
>/gm
,
''
);
}
window
.
gl
.
GfmAutoComplete
=
{
dataSources
:
{},
defaultLoadingData
:
[
'
loading
'
],
cachedData
:
{},
isLoadingData
:
{},
atTypeMap
:
{
'
:
'
:
'
emojis
'
,
'
@
'
:
'
members
'
,
'
#
'
:
'
issues
'
,
'
!
'
:
'
mergeRequests
'
,
'
~
'
:
'
labels
'
,
'
%
'
:
'
milestones
'
,
'
/
'
:
'
commands
'
},
// Emoji
Emoji
:
{
templateFunction
:
function
(
name
)
{
return
`<li>
${
name
}
${
glEmojiTag
(
name
)}
</li>
`
;
}
},
// Team Members
Members
:
{
template
:
'
<li>${avatarTag} ${username} <small>${title}</small></li>
'
},
Labels
:
{
template
:
'
<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>
'
},
// Issues and MergeRequests
Issues
:
{
template
:
'
<li><small>${id}</small> ${title}</li>
'
},
// Milestones
Milestones
:
{
template
:
'
<li>${title}</li>
'
},
Loading
:
{
template
:
'
<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>
'
},
DefaultOptions
:
{
sorter
:
function
(
query
,
items
,
searchKey
)
{
this
.
setting
.
highlightFirst
=
this
.
setting
.
alwaysHighlightFirst
||
query
.
length
>
0
;
if
(
gl
.
GfmAutoComplete
.
isLoading
(
items
))
{
this
.
setting
.
highlightFirst
=
false
;
return
items
;
}
return
$
.
fn
.
atwho
[
"
default
"
].
callbacks
.
sorter
(
query
,
items
,
searchKey
);
},
filter
:
function
(
query
,
data
,
searchKey
)
{
if
(
gl
.
GfmAutoComplete
.
isLoading
(
data
))
{
gl
.
GfmAutoComplete
.
fetchData
(
this
.
$inputor
,
this
.
at
);
return
data
;
}
else
{
return
$
.
fn
.
atwho
[
"
default
"
].
callbacks
.
filter
(
query
,
data
,
searchKey
);
}
},
beforeInsert
:
function
(
value
)
{
if
(
value
&&
!
this
.
setting
.
skipSpecialCharacterTest
)
{
var
withoutAt
=
value
.
substring
(
1
);
if
(
withoutAt
&&
/
[^\w\d]
/
.
test
(
withoutAt
))
value
=
value
.
charAt
()
+
'
"
'
+
withoutAt
+
'
"
'
;
}
return
value
;
},
matcher
:
function
(
flag
,
subtext
)
{
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
var
_a
,
_y
,
regexp
,
match
,
atSymbolsWithBar
,
atSymbolsWithoutBar
;
atSymbolsWithBar
=
Object
.
keys
(
this
.
app
.
controllers
).
join
(
'
|
'
);
atSymbolsWithoutBar
=
Object
.
keys
(
this
.
app
.
controllers
).
join
(
''
);
subtext
=
subtext
.
split
(
/
\s
+/g
).
pop
();
flag
=
flag
.
replace
(
/
[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]
/g
,
"
\\
$&
"
);
_a
=
decodeURI
(
"
%C3%80
"
);
_y
=
decodeURI
(
"
%C3%BF
"
);
regexp
=
new
RegExp
(
"
^(?:
\\
B|[^a-zA-Z0-9_
"
+
atSymbolsWithoutBar
+
"
]|
\\
s)
"
+
flag
+
"
(?!
"
+
atSymbolsWithBar
+
"
)((?:[A-Za-z
"
+
_a
+
"
-
"
+
_y
+
"
0-9_
\
'
\
.
\
+
\
-]|[^
\\
x00-
\\
x7a])*)$
"
,
'
gi
'
);
match
=
regexp
.
exec
(
subtext
);
class
GfmAutoComplete
{
constructor
(
dataSources
)
{
this
.
dataSources
=
dataSources
||
{};
this
.
cachedData
=
{};
this
.
isLoadingData
=
{};
}
if
(
match
)
{
return
match
[
1
];
}
else
{
return
null
;
}
}
},
setup
:
function
(
input
,
enableMap
=
{
setup
(
input
,
enableMap
=
{
emojis
:
true
,
members
:
true
,
issues
:
true
,
milestones
:
true
,
mergeRequests
:
true
,
labels
:
true
labels
:
true
,
})
{
// Add GFM auto-completion to all input fields, that accept GFM input.
this
.
input
=
input
||
$
(
'
.js-gfm-input
'
);
this
.
enableMap
=
enableMap
;
this
.
setupLifecycle
();
},
}
setupLifecycle
()
{
this
.
input
.
each
((
i
,
input
)
=>
{
const
$input
=
$
(
input
);
...
...
@@ -122,9 +36,9 @@ window.gl.GfmAutoComplete = {
// Needed for slash commands with suffixes (ex: /label ~)
$input
.
on
(
'
inserted-commands.atwho
'
,
$input
.
trigger
.
bind
(
$input
,
'
keyup
'
));
});
}
,
}
setupAtWho
:
function
(
$input
)
{
setupAtWho
(
$input
)
{
if
(
this
.
enableMap
.
emojis
)
this
.
setupEmoji
(
$input
);
if
(
this
.
enableMap
.
members
)
this
.
setupMembers
(
$input
);
if
(
this
.
enableMap
.
issues
)
this
.
setupIssues
(
$input
);
...
...
@@ -138,10 +52,11 @@ window.gl.GfmAutoComplete = {
alias
:
'
commands
'
,
searchKey
:
'
search
'
,
skipSpecialCharacterTest
:
true
,
data
:
this
.
defaultLoadingData
,
displayTpl
:
function
(
value
)
{
if
(
this
.
isLoading
(
value
))
return
this
.
Loading
.
template
;
var
tpl
=
'
<li>/${name}
'
;
data
:
GfmAutoComplete
.
defaultLoadingData
,
displayTpl
(
value
)
{
if
(
GfmAutoComplete
.
isLoading
(
value
))
return
GfmAutoComplete
.
Loading
.
template
;
// eslint-disable-next-line no-template-curly-in-string
let
tpl
=
'
<li>/${name}
'
;
if
(
value
.
aliases
.
length
>
0
)
{
tpl
+=
'
<small>(or /<%- aliases.join(", /") %>)</small>
'
;
}
...
...
@@ -153,105 +68,106 @@ window.gl.GfmAutoComplete = {
}
tpl
+=
'
</li>
'
;
return
_
.
template
(
tpl
)(
value
);
}.
bind
(
this
),
insertTpl
:
function
(
value
)
{
var
tpl
=
"
/${name}
"
;
var
reference_prefix
=
null
;
},
insertTpl
(
value
)
{
// eslint-disable-next-line no-template-curly-in-string
let
tpl
=
'
/${name}
'
;
let
referencePrefix
=
null
;
if
(
value
.
params
.
length
>
0
)
{
reference
_p
refix
=
value
.
params
[
0
][
0
];
if
(
/^
[
@%~
]
/
.
test
(
reference
_p
refix
))
{
tpl
+=
'
<%- reference
_p
refix %>
'
;
reference
P
refix
=
value
.
params
[
0
][
0
];
if
(
/^
[
@%~
]
/
.
test
(
reference
P
refix
))
{
tpl
+=
'
<%- reference
P
refix %>
'
;
}
}
return
_
.
template
(
tpl
)({
reference
_prefix
:
reference_p
refix
});
return
_
.
template
(
tpl
)({
reference
P
refix
});
},
suffix
:
''
,
callbacks
:
{
sorter
:
this
.
DefaultOptions
.
sorter
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
beforeSave
:
function
(
commands
)
{
if
(
gl
.
GfmAutoComplete
.
isLoading
(
commands
))
return
commands
;
return
$
.
map
(
commands
,
function
(
c
)
{
var
search
=
c
.
name
;
...
this
.
getDefaultCallbacks
(),
beforeSave
(
commands
)
{
if
(
GfmAutoComplete
.
isLoading
(
commands
))
return
commands
;
return
$
.
map
(
commands
,
(
c
)
=>
{
let
search
=
c
.
name
;
if
(
c
.
aliases
.
length
>
0
)
{
search
=
search
+
"
"
+
c
.
aliases
.
join
(
"
"
)
;
search
=
`
${
search
}
${
c
.
aliases
.
join
(
'
'
)}
`
;
}
return
{
name
:
c
.
name
,
aliases
:
c
.
aliases
,
params
:
c
.
params
,
description
:
c
.
description
,
search
:
search
search
,
};
});
},
matcher
:
function
(
flag
,
subtext
,
should_startWithSpace
,
acceptSpaceBar
)
{
var
regexp
=
/
(?:
^|
\n)\/([
A-Za-z_
]
*
)
$/gi
;
var
match
=
regexp
.
exec
(
subtext
);
matcher
(
flag
,
subtext
)
{
const
regexp
=
/
(?:
^|
\n)\/([
A-Za-z_
]
*
)
$/gi
;
const
match
=
regexp
.
exec
(
subtext
);
if
(
match
)
{
return
match
[
1
];
}
else
{
return
null
;
}
}
}
return
null
;
},
},
});
return
;
},
}
setupEmoji
(
$input
)
{
// Emoji
$input
.
atwho
({
at
:
'
:
'
,
displayTpl
:
function
(
value
)
{
return
value
&&
value
.
name
?
this
.
Emoji
.
templateFunction
(
value
.
name
)
:
this
.
Loading
.
template
;
}.
bind
(
this
),
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
if
(
value
&&
value
.
name
)
{
tmpl
=
GfmAutoComplete
.
Emoji
.
templateFunction
(
value
.
name
);
}
return
tmpl
;
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
:${name}:
'
,
skipSpecialCharacterTest
:
true
,
data
:
this
.
defaultLoadingData
,
data
:
GfmAutoComplete
.
defaultLoadingData
,
callbacks
:
{
sorter
:
this
.
DefaultOptions
.
sorter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
filter
:
this
.
DefaultOptions
.
filter
,
matcher
:
(
flag
,
subtext
)
=>
{
...
this
.
getDefaultCallbacks
(),
matcher
(
flag
,
subtext
)
{
const
relevantText
=
subtext
.
trim
().
split
(
/
\s
/
).
pop
();
const
regexp
=
new
RegExp
(
`(?:[^
${
glRegexp
.
unicodeLetters
}
0-9:]|\n|^):([^:]*)$`
,
'
gi
'
);
const
match
=
regexp
.
exec
(
relevantText
);
return
match
&&
match
.
length
?
match
[
1
]
:
null
;
}
}
}
,
}
,
});
}
,
}
setupMembers
(
$input
)
{
// Team Members
$input
.
atwho
({
at
:
'
@
'
,
displayTpl
:
function
(
value
)
{
return
value
.
username
!=
null
?
this
.
Members
.
template
:
this
.
Loading
.
template
;
}.
bind
(
this
),
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
if
(
value
.
username
!=
null
)
{
tmpl
=
GfmAutoComplete
.
Members
.
template
;
}
return
tmpl
;
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
${atwho-at}${username}
'
,
searchKey
:
'
search
'
,
alwaysHighlightFirst
:
true
,
skipSpecialCharacterTest
:
true
,
data
:
this
.
defaultLoadingData
,
data
:
GfmAutoComplete
.
defaultLoadingData
,
callbacks
:
{
sorter
:
this
.
DefaultOptions
.
sorter
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
matcher
:
this
.
DefaultOptions
.
matcher
,
beforeSave
:
function
(
members
)
{
return
$
.
map
(
members
,
function
(
m
)
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
members
)
{
return
$
.
map
(
members
,
(
m
)
=>
{
let
title
=
''
;
if
(
m
.
username
==
null
)
{
return
m
;
}
title
=
m
.
name
;
if
(
m
.
count
)
{
title
+=
"
(
"
+
m
.
count
+
"
)
"
;
title
+=
` (
${
m
.
count
}
)`
;
}
const
autoCompleteAvatar
=
m
.
avatar_url
||
m
.
username
.
charAt
(
0
).
toUpperCase
();
...
...
@@ -262,173 +178,271 @@ window.gl.GfmAutoComplete = {
username
:
m
.
username
,
avatarTag
:
autoCompleteAvatar
.
length
===
1
?
txtAvatar
:
imgAvatar
,
title
:
sanitize
(
title
),
search
:
sanitize
(
m
.
username
+
"
"
+
m
.
name
)
search
:
sanitize
(
`
${
m
.
username
}
${
m
.
name
}
`
),
};
});
}
}
}
,
}
,
});
}
,
}
setupIssues
(
$input
)
{
$input
.
atwho
({
at
:
'
#
'
,
alias
:
'
issues
'
,
searchKey
:
'
search
'
,
displayTpl
:
function
(
value
)
{
return
value
.
title
!=
null
?
this
.
Issues
.
template
:
this
.
Loading
.
template
;
}.
bind
(
this
),
data
:
this
.
defaultLoadingData
,
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
if
(
value
.
title
!=
null
)
{
tmpl
=
GfmAutoComplete
.
Issues
.
template
;
}
return
tmpl
;
},
data
:
GfmAutoComplete
.
defaultLoadingData
,
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
${atwho-at}${id}
'
,
callbacks
:
{
sorter
:
this
.
DefaultOptions
.
sorter
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
matcher
:
this
.
DefaultOptions
.
matcher
,
beforeSave
:
function
(
issues
)
{
return
$
.
map
(
issues
,
function
(
i
)
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
issues
)
{
return
$
.
map
(
issues
,
(
i
)
=>
{
if
(
i
.
title
==
null
)
{
return
i
;
}
return
{
id
:
i
.
iid
,
title
:
sanitize
(
i
.
title
),
search
:
i
.
iid
+
"
"
+
i
.
title
search
:
`
${
i
.
iid
}
${
i
.
title
}
`
,
};
});
}
}
}
,
}
,
});
}
,
}
setupMilestones
(
$input
)
{
$input
.
atwho
({
at
:
'
%
'
,
alias
:
'
milestones
'
,
searchKey
:
'
search
'
,
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
${atwho-at}${title}
'
,
displayTpl
:
function
(
value
)
{
return
value
.
title
!=
null
?
this
.
Milestones
.
template
:
this
.
Loading
.
template
;
}.
bind
(
this
),
data
:
this
.
defaultLoadingData
,
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
if
(
value
.
title
!=
null
)
{
tmpl
=
GfmAutoComplete
.
Milestones
.
template
;
}
return
tmpl
;
},
data
:
GfmAutoComplete
.
defaultLoadingData
,
callbacks
:
{
matcher
:
this
.
DefaultOptions
.
matcher
,
sorter
:
this
.
DefaultOptions
.
sorter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeSave
:
function
(
milestones
)
{
return
$
.
map
(
milestones
,
function
(
m
)
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
milestones
)
{
return
$
.
map
(
milestones
,
(
m
)
=>
{
if
(
m
.
title
==
null
)
{
return
m
;
}
return
{
id
:
m
.
iid
,
title
:
sanitize
(
m
.
title
),
search
:
""
+
m
.
title
search
:
m
.
title
,
};
});
}
}
}
,
}
,
});
}
,
}
setupMergeRequests
(
$input
)
{
$input
.
atwho
({
at
:
'
!
'
,
alias
:
'
mergerequests
'
,
searchKey
:
'
search
'
,
displayTpl
:
function
(
value
)
{
return
value
.
title
!=
null
?
this
.
Issues
.
template
:
this
.
Loading
.
template
;
}.
bind
(
this
),
data
:
this
.
defaultLoadingData
,
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
if
(
value
.
title
!=
null
)
{
tmpl
=
GfmAutoComplete
.
Issues
.
template
;
}
return
tmpl
;
},
data
:
GfmAutoComplete
.
defaultLoadingData
,
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
${atwho-at}${id}
'
,
callbacks
:
{
sorter
:
this
.
DefaultOptions
.
sorter
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
matcher
:
this
.
DefaultOptions
.
matcher
,
beforeSave
:
function
(
merges
)
{
return
$
.
map
(
merges
,
function
(
m
)
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
merges
)
{
return
$
.
map
(
merges
,
(
m
)
=>
{
if
(
m
.
title
==
null
)
{
return
m
;
}
return
{
id
:
m
.
iid
,
title
:
sanitize
(
m
.
title
),
search
:
m
.
iid
+
"
"
+
m
.
title
search
:
`
${
m
.
iid
}
${
m
.
title
}
`
,
};
});
}
}
}
,
}
,
});
}
,
}
setupLabels
(
$input
)
{
$input
.
atwho
({
at
:
'
~
'
,
alias
:
'
labels
'
,
searchKey
:
'
search
'
,
data
:
this
.
defaultLoadingData
,
displayTpl
:
function
(
value
)
{
return
this
.
isLoading
(
value
)
?
this
.
Loading
.
template
:
this
.
Labels
.
template
;
}.
bind
(
this
),
data
:
GfmAutoComplete
.
defaultLoadingData
,
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Labels
.
template
;
if
(
GfmAutoComplete
.
isLoading
(
value
))
{
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
}
return
tmpl
;
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
${atwho-at}${title}
'
,
callbacks
:
{
matcher
:
this
.
DefaultOptions
.
matcher
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
filter
:
this
.
DefaultOptions
.
filter
,
sorter
:
this
.
DefaultOptions
.
sorter
,
beforeSave
:
function
(
merges
)
{
if
(
gl
.
GfmAutoComplete
.
isLoading
(
merges
))
return
merges
;
var
sanitizeLabelTitle
;
sanitizeLabelTitle
=
function
(
title
)
{
if
(
/
[\w\?
&
]
+
\s
+
[\w\?
&
]
+/g
.
test
(
title
))
{
return
"
\"
"
+
(
sanitize
(
title
))
+
"
\"
"
;
}
else
{
return
sanitize
(
title
);
}
};
return
$
.
map
(
merges
,
function
(
m
)
{
return
{
title
:
sanitize
(
m
.
title
),
color
:
m
.
color
,
search
:
""
+
m
.
title
};
});
}
}
...
this
.
getDefaultCallbacks
(),
beforeSave
(
merges
)
{
if
(
GfmAutoComplete
.
isLoading
(
merges
))
return
merges
;
return
$
.
map
(
merges
,
m
=>
({
title
:
sanitize
(
m
.
title
),
color
:
m
.
color
,
search
:
m
.
title
,
}));
},
},
});
}
,
}
fetchData
:
function
(
$input
,
at
)
{
getDefaultCallbacks
()
{
const
fetchData
=
this
.
fetchData
.
bind
(
this
);
return
{
sorter
(
query
,
items
,
searchKey
)
{
this
.
setting
.
highlightFirst
=
this
.
setting
.
alwaysHighlightFirst
||
query
.
length
>
0
;
if
(
GfmAutoComplete
.
isLoading
(
items
))
{
this
.
setting
.
highlightFirst
=
false
;
return
items
;
}
return
$
.
fn
.
atwho
.
default
.
callbacks
.
sorter
(
query
,
items
,
searchKey
);
},
filter
(
query
,
data
,
searchKey
)
{
if
(
GfmAutoComplete
.
isLoading
(
data
))
{
fetchData
(
this
.
$inputor
,
this
.
at
);
return
data
;
}
return
$
.
fn
.
atwho
.
default
.
callbacks
.
filter
(
query
,
data
,
searchKey
);
},
beforeInsert
(
value
)
{
let
resultantValue
=
value
;
if
(
value
&&
!
this
.
setting
.
skipSpecialCharacterTest
)
{
const
withoutAt
=
value
.
substring
(
1
);
if
(
withoutAt
&&
/
[^\w\d]
/
.
test
(
withoutAt
))
{
resultantValue
=
`
${
value
.
charAt
()}
"
${
withoutAt
}
"`
;
}
}
return
resultantValue
;
},
matcher
(
flag
,
subtext
)
{
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
const
atSymbolsWithBar
=
Object
.
keys
(
this
.
app
.
controllers
).
join
(
'
|
'
);
const
atSymbolsWithoutBar
=
Object
.
keys
(
this
.
app
.
controllers
).
join
(
''
);
const
targetSubtext
=
subtext
.
split
(
/
\s
+/g
).
pop
();
const
resultantFlag
=
flag
.
replace
(
/
[
-[
\]/
{}()*+?.
\\
^$|
]
/g
,
'
\\
$&
'
);
const
accentAChar
=
decodeURI
(
'
%C3%80
'
);
const
accentYChar
=
decodeURI
(
'
%C3%BF
'
);
const
regexp
=
new
RegExp
(
`^(?:\\B|[^a-zA-Z0-9_
${
atSymbolsWithoutBar
}
]|\\s)
${
resultantFlag
}
(?!
${
atSymbolsWithBar
}
)((?:[A-Za-z
${
accentAChar
}
-
${
accentYChar
}
0-9_'.+-]|[^\\x00-\\x7a])*)$`
,
'
gi
'
);
const
match
=
regexp
.
exec
(
targetSubtext
);
if
(
match
)
{
return
match
[
1
];
}
return
null
;
},
};
}
fetchData
(
$input
,
at
)
{
if
(
this
.
isLoadingData
[
at
])
return
;
this
.
isLoadingData
[
at
]
=
true
;
if
(
this
.
cachedData
[
at
])
{
this
.
loadData
(
$input
,
at
,
this
.
cachedData
[
at
]);
}
else
if
(
this
.
atTypeMap
[
at
]
===
'
emojis
'
)
{
}
else
if
(
GfmAutoComplete
.
atTypeMap
[
at
]
===
'
emojis
'
)
{
this
.
loadData
(
$input
,
at
,
Object
.
keys
(
emojiMap
).
concat
(
Object
.
keys
(
emojiAliases
)));
}
else
{
$
.
getJSON
(
this
.
dataSources
[
this
.
atTypeMap
[
at
]],
(
data
)
=>
{
$
.
getJSON
(
this
.
dataSources
[
GfmAutoComplete
.
atTypeMap
[
at
]],
(
data
)
=>
{
this
.
loadData
(
$input
,
at
,
data
);
}).
fail
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
}
}
,
loadData
:
function
(
$input
,
at
,
data
)
{
}
loadData
(
$input
,
at
,
data
)
{
this
.
isLoadingData
[
at
]
=
false
;
this
.
cachedData
[
at
]
=
data
;
$input
.
atwho
(
'
load
'
,
at
,
data
);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
return
$input
.
trigger
(
'
keyup
'
);
},
isLoading
(
data
)
{
var
dataToInspect
=
data
;
}
static
isLoading
(
data
)
{
let
dataToInspect
=
data
;
if
(
data
&&
data
.
length
>
0
)
{
dataToInspect
=
data
[
0
];
}
var
loadingState
=
this
.
defaultLoadingData
[
0
];
const
loadingState
=
GfmAutoComplete
.
defaultLoadingData
[
0
];
return
dataToInspect
&&
(
dataToInspect
===
loadingState
||
dataToInspect
.
name
===
loadingState
);
}
}
GfmAutoComplete
.
defaultLoadingData
=
[
'
loading
'
];
GfmAutoComplete
.
atTypeMap
=
{
'
:
'
:
'
emojis
'
,
'
@
'
:
'
members
'
,
'
#
'
:
'
issues
'
,
'
!
'
:
'
mergeRequests
'
,
'
~
'
:
'
labels
'
,
'
%
'
:
'
milestones
'
,
'
/
'
:
'
commands
'
,
};
// Emoji
GfmAutoComplete
.
Emoji
=
{
templateFunction
(
name
)
{
return
`<li>
${
name
}
${
glEmojiTag
(
name
)}
</li>
`
;
},
};
// Team Members
GfmAutoComplete
.
Members
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li>${avatarTag} ${username} <small>${title}</small></li>
'
,
};
GfmAutoComplete
.
Labels
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>
'
,
};
// Issues and MergeRequests
GfmAutoComplete
.
Issues
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li><small>${id}</small> ${title}</li>
'
,
};
// Milestones
GfmAutoComplete
.
Milestones
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li>${title}</li>
'
,
};
GfmAutoComplete
.
Loading
=
{
template
:
'
<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>
'
,
};
export
default
GfmAutoComplete
;
app/assets/javascripts/gl_form.js
View file @
fb462e2e
...
...
@@ -3,6 +3,8 @@
/* global DropzoneInput */
/* global autosize */
import
GfmAutoComplete
from
'
./gfm_auto_complete
'
;
window
.
gl
=
window
.
gl
||
{};
function
GLForm
(
form
)
{
...
...
@@ -31,7 +33,7 @@ GLForm.prototype.setupForm = function() {
// remove notify commit author checkbox for non-commit notes
gl
.
utils
.
disableButtonIfEmptyField
(
this
.
form
.
find
(
'
.js-note-text
'
),
this
.
form
.
find
(
'
.js-comment-button, .js-note-new-discussion
'
));
gl
.
GfmAutoComplete
.
setup
(
this
.
form
.
find
(
'
.js-gfm-input
'
));
new
GfmAutoComplete
(
gl
.
GfmAutoComplete
&&
gl
.
GfmAutoComplete
.
dataSources
)
.
setup
(
this
.
form
.
find
(
'
.js-gfm-input
'
));
new
DropzoneInput
(
this
.
form
);
autosize
(
this
.
textarea
);
}
...
...
app/assets/javascripts/issuable_form.js
View file @
fb462e2e
...
...
@@ -7,6 +7,8 @@
/* global dateFormat */
/* global Pikaday */
import
GfmAutoComplete
from
'
./gfm_auto_complete
'
;
(
function
()
{
this
.
IssuableForm
=
(
function
()
{
IssuableForm
.
prototype
.
issueMoveConfirmMsg
=
'
Are you sure you want to move this issue to another project?
'
;
...
...
@@ -20,7 +22,7 @@
this
.
renderWipExplanation
=
this
.
renderWipExplanation
.
bind
(
this
);
this
.
resetAutosave
=
this
.
resetAutosave
.
bind
(
this
);
this
.
handleSubmit
=
this
.
handleSubmit
.
bind
(
this
);
gl
.
GfmAutoComplete
.
setup
();
new
GfmAutoComplete
(
gl
.
GfmAutoComplete
&&
gl
.
GfmAutoComplete
.
dataSources
)
.
setup
();
new
UsersSelect
();
new
GroupsSelect
();
new
ZenMode
();
...
...
app/assets/javascripts/main.js
View file @
fb462e2e
...
...
@@ -97,7 +97,6 @@ import './dropzone_input';
import
'
./due_date_select
'
;
import
'
./files_comment_button
'
;
import
'
./flash
'
;
import
'
./gfm_auto_complete
'
;
import
'
./gl_dropdown
'
;
import
'
./gl_field_error
'
;
import
'
./gl_field_errors
'
;
...
...
app/assets/javascripts/merge_request_tabs.js
View file @
fb462e2e
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Flash */
/* global notes */
import
Cookies
from
'
js-cookie
'
;
import
'
./breakpoints
'
;
...
...
@@ -251,7 +252,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this
.
ajaxGet
({
url
:
`
${
urlPathname
}
.json
${
location
.
search
}
`
,
success
:
(
data
)
=>
{
$
(
'
#diffs
'
).
html
(
data
.
html
);
const
$container
=
$
(
'
#diffs
'
);
$container
.
html
(
data
.
html
);
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
)
{
gl
.
diffNotesCompileComponents
();
...
...
@@ -278,6 +280,20 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
})
.
init
();
});
// Scroll any linked note into view
// Similar to `toggler_behavior` in the discussion tab
const
hash
=
window
.
gl
.
utils
.
getLocationHash
();
const
anchor
=
hash
&&
$container
.
find
(
`[id="
${
hash
}
"]`
);
if
(
anchor
)
{
const
notesContent
=
anchor
.
closest
(
'
.notes_content
'
);
const
lineType
=
notesContent
.
hasClass
(
'
new
'
)
?
'
new
'
:
'
old
'
;
notes
.
addDiffNote
(
anchor
,
lineType
,
false
);
anchor
[
0
].
scrollIntoView
();
// We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first
anchor
.
addClass
(
'
target
'
);
}
},
});
}
...
...
app/assets/javascripts/notes.js
View file @
fb462e2e
...
...
@@ -12,7 +12,6 @@ require('./autosave');
window
.
autosize
=
require
(
'
vendor/autosize
'
);
window
.
Dropzone
=
require
(
'
dropzone
'
);
require
(
'
./dropzone_input
'
);
require
(
'
./gfm_auto_complete
'
);
require
(
'
vendor/jquery.caret
'
);
// required by jquery.atwho
require
(
'
vendor/jquery.atwho
'
);
require
(
'
./task_list
'
);
...
...
@@ -24,7 +23,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
;
...
...
@@ -33,9 +32,9 @@ const normalizeNewlines = function(str) {
this
.
updateComment
=
this
.
updateComment
.
bind
(
this
);
this
.
visibilityChange
=
this
.
visibilityChange
.
bind
(
this
);
this
.
cancelDiscussionForm
=
this
.
cancelDiscussionForm
.
bind
(
this
);
this
.
addDiffNote
=
this
.
a
ddDiffNote
.
bind
(
this
);
this
.
onAddDiffNote
=
this
.
onA
ddDiffNote
.
bind
(
this
);
this
.
setupDiscussionNoteForm
=
this
.
setupDiscussionNoteForm
.
bind
(
this
);
this
.
replyToDiscussionNote
=
this
.
r
eplyToDiscussionNote
.
bind
(
this
);
this
.
onReplyToDiscussionNote
=
this
.
onR
eplyToDiscussionNote
.
bind
(
this
);
this
.
removeNote
=
this
.
removeNote
.
bind
(
this
);
this
.
cancelEdit
=
this
.
cancelEdit
.
bind
(
this
);
this
.
updateNote
=
this
.
updateNote
.
bind
(
this
);
...
...
@@ -100,9 +99,9 @@ const normalizeNewlines = function(str) {
// update the file name when an attachment is selected
$
(
document
).
on
(
"
change
"
,
"
.js-note-attachment-input
"
,
this
.
updateFormAttachment
);
// reply to diff/discussion notes
$
(
document
).
on
(
"
click
"
,
"
.js-discussion-reply-button
"
,
this
.
r
eplyToDiscussionNote
);
$
(
document
).
on
(
"
click
"
,
"
.js-discussion-reply-button
"
,
this
.
onR
eplyToDiscussionNote
);
// add diff note
$
(
document
).
on
(
"
click
"
,
"
.js-add-diff-note-button
"
,
this
.
a
ddDiffNote
);
$
(
document
).
on
(
"
click
"
,
"
.js-add-diff-note-button
"
,
this
.
onA
ddDiffNote
);
// hide diff note form
$
(
document
).
on
(
"
click
"
,
"
.js-close-discussion-note-form
"
,
this
.
cancelDiscussionForm
);
// toggle commit list
...
...
@@ -794,10 +793,14 @@ const normalizeNewlines = function(str) {
Shows the note form below the notes.
*/
Notes
.
prototype
.
replyToDiscussionNote
=
function
(
e
)
{
Notes
.
prototype
.
onReplyToDiscussionNote
=
function
(
e
)
{
this
.
replyToDiscussionNote
(
e
.
target
);
};
Notes
.
prototype
.
replyToDiscussionNote
=
function
(
target
)
{
var
form
,
replyLink
;
form
=
this
.
cleanForm
(
this
.
formClone
.
clone
());
replyLink
=
$
(
e
.
target
).
closest
(
"
.js-discussion-reply-button
"
);
replyLink
=
$
(
target
).
closest
(
"
.js-discussion-reply-button
"
);
// insert the form after the button
replyLink
.
closest
(
'
.discussion-reply-holder
'
)
...
...
@@ -867,35 +870,43 @@ const normalizeNewlines = function(str) {
Sets up the form and shows it.
*/
Notes
.
prototype
.
addDiffNote
=
function
(
e
)
{
var
$link
,
addForm
,
hasNotes
,
lineType
,
newForm
,
nextRow
,
noteForm
,
notesContent
,
notesContentSelector
,
replyButton
,
row
,
rowCssToAdd
,
targetContent
,
isDiffCommentAvatar
;
Notes
.
prototype
.
onAddDiffNote
=
function
(
e
)
{
e
.
preventDefault
();
$link
=
$
(
e
.
currentTarget
||
e
.
target
);
const
$link
=
$
(
e
.
currentTarget
||
e
.
target
);
const
showReplyInput
=
!
$link
.
hasClass
(
'
js-diff-comment-avatar
'
);
this
.
addDiffNote
(
$link
,
$link
.
data
(
'
lineType
'
),
showReplyInput
);
};
Notes
.
prototype
.
addDiffNote
=
function
(
target
,
lineType
,
showReplyInput
)
{
var
$link
,
addForm
,
hasNotes
,
newForm
,
noteForm
,
replyButton
,
row
,
rowCssToAdd
,
targetContent
,
isDiffCommentAvatar
;
$link
=
$
(
target
);
row
=
$link
.
closest
(
"
tr
"
);
nextRow
=
row
.
next
();
hasNotes
=
nextRow
.
is
(
"
.notes_holder
"
);
const
nextRow
=
row
.
next
();
let
targetRow
=
row
;
if
(
nextRow
.
is
(
'
.notes_holder
'
))
{
targetRow
=
nextRow
;
}
hasNotes
=
targetRow
.
is
(
"
.notes_holder
"
);
addForm
=
false
;
notesContentSelector
=
"
.notes_content
"
;
let
lineTypeSelector
=
''
;
rowCssToAdd
=
"
<tr class=
\"
notes_holder js-temp-notes-holder
\"
><td class=
\"
notes_line
\"
colspan=
\"
2
\"
></td><td class=
\"
notes_content
\"
><div class=
\"
content
\"
></div></td></tr>
"
;
isDiffCommentAvatar
=
$link
.
hasClass
(
'
js-diff-comment-avatar
'
);
// In parallel view, look inside the correct left/right pane
if
(
this
.
isParallelView
())
{
lineType
=
$link
.
data
(
"
lineType
"
);
notesContentSelector
+=
"
.
"
+
lineType
;
lineTypeSelector
=
`.
${
lineType
}
`
;
rowCssToAdd
=
"
<tr class=
\"
notes_holder js-temp-notes-holder
\"
><td class=
\"
notes_line old
\"
></td><td class=
\"
notes_content parallel old
\"
><div class=
\"
content
\"
></div></td><td class=
\"
notes_line new
\"
></td><td class=
\"
notes_content parallel new
\"
><div class=
\"
content
\"
></div></td></tr>
"
;
}
notesContentSelector
+=
"
.content
"
;
notesContent
=
nex
tRow
.
find
(
notesContentSelector
);
const
notesContentSelector
=
`.notes_content
${
lineTypeSelector
}
.content`
;
let
notesContent
=
targe
tRow
.
find
(
notesContentSelector
);
if
(
hasNotes
&&
!
isDiffCommentAvatar
)
{
nex
tRow
.
show
();
notesContent
=
nex
tRow
.
find
(
notesContentSelector
);
if
(
hasNotes
&&
showReplyInput
)
{
targe
tRow
.
show
();
notesContent
=
targe
tRow
.
find
(
notesContentSelector
);
if
(
notesContent
.
length
)
{
notesContent
.
show
();
replyButton
=
notesContent
.
find
(
"
.js-discussion-reply-button:visible
"
);
if
(
replyButton
.
length
)
{
e
.
target
=
replyButton
[
0
];
$
.
proxy
(
this
.
replyToDiscussionNote
,
replyButton
[
0
],
e
).
call
();
this
.
replyToDiscussionNote
(
replyButton
[
0
]);
}
else
{
// In parallel view, the form may not be present in one of the panes
noteForm
=
notesContent
.
find
(
"
.js-discussion-note-form
"
);
...
...
@@ -904,18 +915,18 @@ const normalizeNewlines = function(str) {
}
}
}
}
else
if
(
!
isDiffCommentAvatar
)
{
}
else
if
(
showReplyInput
)
{
// add a notes row and insert the form
row
.
after
(
rowCssToAdd
);
nex
tRow
=
row
.
next
();
notesContent
=
nex
tRow
.
find
(
notesContentSelector
);
targe
tRow
=
row
.
next
();
notesContent
=
targe
tRow
.
find
(
notesContentSelector
);
addForm
=
true
;
}
else
{
nex
tRow
.
show
();
targe
tRow
.
show
();
notesContent
.
toggle
(
!
notesContent
.
is
(
'
:visible
'
));
if
(
!
nex
tRow
.
find
(
'
.content:not(:empty)
'
).
is
(
'
:visible
'
))
{
nex
tRow
.
hide
();
if
(
!
targe
tRow
.
find
(
'
.content:not(:empty)
'
).
is
(
'
:visible
'
))
{
targe
tRow
.
hide
();
}
}
...
...
@@ -1170,6 +1181,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">
...
...
@@ -1190,7 +1202,7 @@ const normalizeNewlines = function(str) {
</div>
<div class="note-body">
<div class="note-text">
<p>
${
f
ormContent
}
</p>
<p>
${
escapedF
ormContent
}
</p>
</div>
</div>
</div>
...
...
@@ -1320,7 +1332,7 @@ const normalizeNewlines = function(str) {
// Show form again on UI on failure
if
(
isDiscussionForm
&&
$notesContainer
.
length
)
{
const
replyButton
=
$notesContainer
.
parent
().
find
(
'
.js-discussion-reply-button
'
);
$
.
proxy
(
this
.
replyToDiscussionNote
,
replyButton
[
0
],
{
target
:
replyButton
[
0
]
}).
call
(
);
this
.
replyToDiscussionNote
(
replyButton
[
0
]
);
$form
=
$notesContainer
.
parent
().
find
(
'
form
'
);
}
...
...
app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
View file @
fb462e2e
...
...
@@ -34,7 +34,7 @@ export default {
<div class="mr-widget-heading">
<div class="ci-widget">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed js-ci-error">
<div class="ci-status-icon ci-status-icon-failed
ci-error
js-ci-error">
<span class="js-icon-link icon-link">
<span
v-html="svg"
...
...
app/assets/stylesheets/framework/timeline.scss
View file @
fb462e2e
...
...
@@ -3,30 +3,6 @@
margin
:
0
;
padding
:
0
;
.timeline-entry
{
padding
:
$gl-padding
$gl-btn-padding
0
;
border-color
:
$white-normal
;
color
:
$gl-text-color
;
border-bottom
:
1px
solid
$border-white-light
;
.timeline-entry-inner
{
position
:
relative
;
}
&
:target
{
background
:
$line-target-blue
;
}
.avatar
{
margin-right
:
15px
;
}
.controls
{
padding-top
:
10px
;
float
:
right
;
}
}
.note-text
{
p
:last-child
{
margin-bottom
:
0
;
...
...
@@ -46,20 +22,45 @@
}
}
.timeline-entry
{
padding
:
$gl-padding
$gl-btn-padding
0
;
border-color
:
$white-normal
;
color
:
$gl-text-color
;
border-bottom
:
1px
solid
$border-white-light
;
.timeline-entry-inner
{
position
:
relative
;
}
&
:target
,
&
.target
{
background
:
$line-target-blue
;
}
.avatar
{
margin-right
:
15px
;
}
.controls
{
padding-top
:
10px
;
float
:
right
;
}
}
@media
(
max-width
:
$screen-xs-max
)
{
.timeline
{
&
:
:
before
{
background
:
none
;
}
}
.timeline-entry
.timeline-entry-inner
{
.timeline-icon
{
display
:
none
;
}
.timeline-entry
.timeline-entry-inner
{
.timeline-icon
{
display
:
none
;
}
.timeline-content
{
margin-left
:
0
;
}
.timeline-content
{
margin-left
:
0
;
}
}
}
...
...
app/assets/stylesheets/pages/merge_requests.scss
View file @
fb462e2e
...
...
@@ -109,6 +109,10 @@
height
:
22px
;
margin-right
:
8px
;
}
.ci-error
{
margin-right
:
$btn-side-margin
;
}
}
.mr-widget-body
,
...
...
app/controllers/projects/issues_controller.rb
View file @
fb462e2e
...
...
@@ -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
,
:re
ndered_title
,
:create_merge_request
]
:related_branches
,
:can_create_branch
,
:re
altime_changes
,
:create_merge_request
]
# Allow read any issue
before_action
:authorize_read_issue!
,
only:
[
:show
,
:re
ndered_title
]
before_action
:authorize_read_issue!
,
only:
[
:show
,
:re
altime_changes
]
# Allow write(create) issue
before_action
:authorize_create_issue!
,
only:
[
:new
,
:create
]
...
...
@@ -207,7 +207,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def
re
ndered_title
def
re
altime_changes
Gitlab
::
PollingInterval
.
set_header
(
response
,
interval:
3_000
)
render
json:
{
...
...
app/models/ee/user.rb
View file @
fb462e2e
...
...
@@ -52,5 +52,15 @@ module EE
def
admin_or_auditor?
admin?
||
auditor?
end
def
remember_me!
return
if
::
Gitlab
::
Geo
.
secondary?
super
end
def
forget_me!
return
if
::
Gitlab
::
Geo
.
secondary?
super
end
end
end
app/models/issue.rb
View file @
fb462e2e
...
...
@@ -312,7 +312,7 @@ class Issue < ActiveRecord::Base
end
def
expire_etag_cache
key
=
Gitlab
::
Routing
.
url_helpers
.
re
ndered_title
_namespace_project_issue_path
(
key
=
Gitlab
::
Routing
.
url_helpers
.
re
altime_changes
_namespace_project_issue_path
(
project
.
namespace
,
project
,
self
...
...
app/services/geo/repository_
backfill
_service.rb
→
app/services/geo/repository_
sync
_service.rb
View file @
fb462e2e
module
Geo
class
Repository
Backfill
Service
class
Repository
Sync
Service
attr_reader
:project_id
LEASE_TIMEOUT
=
8
.
hours
.
freeze
LEASE_KEY_PREFIX
=
'repository_
backfill
_service'
.
freeze
LEASE_KEY_PREFIX
=
'repository_
sync
_service'
.
freeze
def
initialize
(
project_id
)
@project_id
=
project_id
...
...
@@ -81,7 +81,7 @@ module Geo
end
def
update_registry
(
started_at
,
finished_at
)
log
(
'Updating re
gistry
information'
)
log
(
'Updating re
pository sync
information'
)
registry
=
Geo
::
ProjectRegistry
.
find_or_initialize_by
(
project_id:
project_id
)
registry
.
last_repository_synced_at
=
started_at
registry
.
last_repository_successful_sync_at
=
finished_at
if
finished_at
...
...
app/views/layouts/_init_auto_complete.html.haml
View file @
fb462e2e
...
...
@@ -3,6 +3,7 @@
-
if
project
:javascript
gl
.
GfmAutoComplete
=
gl
.
GfmAutoComplete
||
{};
gl
.
GfmAutoComplete
.
dataSources
=
{
members
:
"
#{
members_namespace_project_autocomplete_sources_path
(
project
.
namespace
,
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
,
issues
:
"
#{
issues_namespace_project_autocomplete_sources_path
(
project
.
namespace
,
project
)
}
"
,
...
...
@@ -11,5 +12,3 @@
milestones
:
"
#{
milestones_namespace_project_autocomplete_sources_path
(
project
.
namespace
,
project
)
}
"
,
commands
:
"
#{
commands_namespace_project_autocomplete_sources_path
(
project
.
namespace
,
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
};
gl
.
GfmAutoComplete
.
setup
();
app/views/layouts/application.html.haml
View file @
fb462e2e
...
...
@@ -2,8 +2,8 @@
%html
{
lang:
I18n
.
locale
,
class:
"#{page_class}"
}
=
render
"layouts/head"
%body
{
class:
@body_class
,
data:
{
page:
body_data_page
,
project:
"#{@project.path if @project}"
,
group:
"#{@group.path if @group}"
}
}
=
render
"layouts/init_auto_complete"
if
@gfm_form
=
render
"layouts/header/default"
,
title:
header_title
=
render
'layouts/page'
,
sidebar:
sidebar
,
nav:
nav
=
yield
:scripts_body
=
render
"layouts/init_auto_complete"
if
@gfm_form
app/views/projects/boards/_show.html.haml
View file @
fb462e2e
...
...
@@ -4,9 +4,9 @@
-
page_title
"Boards"
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'common_vue'
)
=
page_specific_javascript_bundle_tag
(
'filtered_search'
)
=
page_specific_javascript_bundle_tag
(
'boards'
)
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'filtered_search'
=
webpack_bundle_tag
'boards'
%script
#js-board-template
{
type:
"text/x-template"
}=
render
"projects/boards/components/board"
%script
#js-board-modal-filter
{
type:
"text/x-template"
}=
render
"shared/issuable/search_bar"
,
type: :boards_modal
...
...
app/views/projects/issues/index.html.haml
View file @
fb462e2e
...
...
@@ -7,8 +7,9 @@
=
render
"projects/issues/head"
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'filtered_search'
)
=
page_specific_javascript_bundle_tag
(
'issues'
)
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'filtered_search'
=
webpack_bundle_tag
'issues'
=
content_for
:meta_tags
do
=
auto_discovery_link_tag
(
:atom
,
params
.
merge
(
rss_url_options
),
title:
"
#{
@project
.
name
}
issues"
)
...
...
app/views/projects/issues/show.html.haml
View file @
fb462e2e
...
...
@@ -51,7 +51,7 @@
.issue-details.issuable-details
.detail-page-description.content-block
{
class:
(
'hide-bottom-border'
unless
@issue
.
description
.
present?
)
}
.issue-title-data.hidden
{
"data"
=>
{
"endpoint"
=>
re
ndered_title
_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
.issue-title-data.hidden
{
"data"
=>
{
"endpoint"
=>
re
altime_changes
_namespace_project_issue_path
(
@project
.
namespace
,
@project
,
@issue
),
"can-update-tasks-class"
=>
can?
(
current_user
,
:update_issue
,
@issue
)
?
'js-task-list-container'
:
''
,
}
}
.issue-title-entrypoint
...
...
app/views/projects/merge_requests/_show.html.haml
View file @
fb462e2e
...
...
@@ -27,7 +27,8 @@
#js-vue-mr-widget
.mr-widget
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'vue_merge_request_widget'
)
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container
=
render
'award_emoji/awards_block'
,
awardable:
@merge_request
,
inline:
true
...
...
app/views/projects/merge_requests/index.html.haml
View file @
fb462e2e
...
...
@@ -8,7 +8,8 @@
=
render
'projects/last_push'
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'filtered_search'
)
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'filtered_search'
-
if
@project
.
merge_requests
.
exists?
%div
{
class:
container_class
}
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
fb462e2e
...
...
@@ -23,7 +23,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
...
...
app/workers/geo_
backfill
_worker.rb
→
app/workers/geo_
repository_sync
_worker.rb
View file @
fb462e2e
class
Geo
Backfill
Worker
class
Geo
RepositorySync
Worker
include
Sidekiq
::
Worker
include
CronjobQueue
...
...
@@ -15,20 +15,20 @@ class GeoBackfillWorker
project_ids_updated_recently
=
find_synced_project_ids_updated_recently
project_ids
=
interleave
(
project_ids_not_synced
,
project_ids_updated_recently
)
logger
.
info
"Started Geo
backfill
ing for
#{
project_ids
.
length
}
project(s)"
logger
.
info
"Started Geo
repository sync
ing for
#{
project_ids
.
length
}
project(s)"
project_ids
.
each
do
|
project_id
|
begin
break
if
over_time?
(
start_time
)
break
unless
Gitlab
::
Geo
.
current_node_enabled?
# We try to obtain a lease here for the entire
backfilling process
#
because backfill
the repositories continuously at a controlled rate
# instead of hammering the primary node. Initially, we are
backfill
ing
# We try to obtain a lease here for the entire
sync process because we
#
want to sync
the repositories continuously at a controlled rate
# instead of hammering the primary node. Initially, we are
sync
ing
# one repo at a time. If we don't obtain the lease here, every 5
# minutes all of 100 projects will be synced.
try_obtain_lease
do
|
lease
|
Geo
::
Repository
Backfill
Service
.
new
(
project_id
).
execute
Geo
::
Repository
Sync
Service
.
new
(
project_id
).
execute
end
rescue
ActiveRecord
::
RecordNotFound
logger
.
error
(
"Couldn't find project with ID=
#{
project_id
}
, skipping syncing"
)
...
...
@@ -36,7 +36,7 @@ class GeoBackfillWorker
end
end
logger
.
info
"Finished Geo
backfill
ing for
#{
project_ids
.
length
}
project(s)"
logger
.
info
"Finished Geo
repository sync
ing for
#{
project_ids
.
length
}
project(s)"
end
private
...
...
@@ -86,10 +86,10 @@ class GeoBackfillWorker
end
def
lease_key
Geo
::
Repository
Backfill
Service
::
LEASE_KEY_PREFIX
Geo
::
Repository
Sync
Service
::
LEASE_KEY_PREFIX
end
def
lease_timeout
Geo
::
Repository
Backfill
Service
::
LEASE_TIMEOUT
Geo
::
Repository
Sync
Service
::
LEASE_TIMEOUT
end
end
changelogs/unreleased-ee/2366-geo-sign-out-broken.yml
0 → 100644
View file @
fb462e2e
---
title
:
Geo - Fix signing out from secondary node when "Remember me" option is checked
merge_request
:
1903
author
:
changelogs/unreleased/31902-namespace-recent-searches-to-project.yml
0 → 100644
View file @
fb462e2e
---
title
:
Scope issue/merge request recent searches to project
merge_request
:
author
:
config/gitlab.yml.example
View file @
fb462e2e
...
...
@@ -216,14 +216,14 @@ production: &base
geo_bulk_notify_worker:
cron: "*/10 * * * * *"
# GitLab Geo
backfill
worker
# GitLab Geo
repository sync
worker
# NOTE: This will only take effect if Geo is enabled
geo_
backfill
_worker:
geo_
repository_sync
_worker:
cron: "*/5 * * * *"
# GitLab Geo file download worker
# GitLab Geo file download
dispatch
worker
# NOTE: This will only take effect if Geo is enabled
geo_download_dispatch_worker:
geo_
file_
download_dispatch_worker:
cron: "*/10 * * * *"
registry:
...
...
config/initializers/1_settings.rb
View file @
fb462e2e
...
...
@@ -396,12 +396,12 @@ Settings.cron_jobs['ldap_group_sync_worker']['job_class'] = 'LdapGroupSyncWorker
Settings
.
cron_jobs
[
'geo_bulk_notify_worker'
]
||=
Settingslogic
.
new
({})
Settings
.
cron_jobs
[
'geo_bulk_notify_worker'
][
'cron'
]
||=
'*/10 * * * * *'
Settings
.
cron_jobs
[
'geo_bulk_notify_worker'
][
'job_class'
]
||=
'GeoBulkNotifyWorker'
Settings
.
cron_jobs
[
'geo_
backfill
_worker'
]
||=
Settingslogic
.
new
({})
Settings
.
cron_jobs
[
'geo_
backfill
_worker'
][
'cron'
]
||=
'*/5 * * * *'
Settings
.
cron_jobs
[
'geo_
backfill_worker'
][
'job_class'
]
||=
'GeoBackfill
Worker'
Settings
.
cron_jobs
[
'geo_download_dispatch_worker'
]
||=
Settingslogic
.
new
({})
Settings
.
cron_jobs
[
'geo_download_dispatch_worker'
][
'cron'
]
||=
'5 * * * *'
Settings
.
cron_jobs
[
'geo_download_dispatch_worker'
][
'job_class'
]
||=
'GeoFileDownloadDispatchWorker'
Settings
.
cron_jobs
[
'geo_
repository_sync
_worker'
]
||=
Settingslogic
.
new
({})
Settings
.
cron_jobs
[
'geo_
repository_sync
_worker'
][
'cron'
]
||=
'*/5 * * * *'
Settings
.
cron_jobs
[
'geo_
repository_sync_worker'
][
'job_class'
]
||=
'GeoRepositorySync
Worker'
Settings
.
cron_jobs
[
'geo_
file_
download_dispatch_worker'
]
||=
Settingslogic
.
new
({})
Settings
.
cron_jobs
[
'geo_
file_
download_dispatch_worker'
][
'cron'
]
||=
'5 * * * *'
Settings
.
cron_jobs
[
'geo_
file_
download_dispatch_worker'
][
'job_class'
]
||=
'GeoFileDownloadDispatchWorker'
Settings
.
cron_jobs
[
'gitlab_usage_ping_worker'
]
||=
Settingslogic
.
new
({})
Settings
.
cron_jobs
[
'gitlab_usage_ping_worker'
][
'cron'
]
||=
Settings
.
send
(
:cron_random_weekly_time
)
Settings
.
cron_jobs
[
'gitlab_usage_ping_worker'
][
'job_class'
]
=
'GitlabUsagePingWorker'
...
...
config/routes/project.rb
View file @
fb462e2e
...
...
@@ -279,7 +279,7 @@ constraints(ProjectUrlConstrainer.new) do
get
:referenced_merge_requests
get
:related_branches
get
:can_create_branch
get
:re
ndered_title
get
:re
altime_changes
post
:create_merge_request
end
collection
do
...
...
config/webpack.config.js
View file @
fb462e2e
...
...
@@ -143,16 +143,24 @@ var config = {
'
diff_notes
'
,
'
environments
'
,
'
environments_folder
'
,
'
sidebar
'
,
'
filtered_search
'
,
'
issue_show
'
,
'
merge_conflicts
'
,
'
notebook_viewer
'
,
'
pdf_viewer
'
,
'
pipelines
'
,
<<<<<<<
HEAD
'
mr_widget_ee
'
,
'
issue_show
'
,
'
balsamiq_viewer
'
,
'
pipelines_graph
'
,
=======
'
pipelines_graph
'
,
'
schedule_form
'
,
'
schedules_index
'
,
'
sidebar
'
,
'
vue_merge_request_widget
'
,
>>>>>>>
origin
/
master
],
minChunks
:
function
(
module
,
count
)
{
return
module
.
resource
&&
(
/vue_shared/
).
test
(
module
.
resource
);
...
...
doc/development/writing_documentation.md
View file @
fb462e2e
...
...
@@ -76,14 +76,21 @@ Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, b
We try to treat documentation as code, thus have implemented some testing.
Currently, the following tests are in place:
1.
`docs
:check:links`
: Check that all internal (relative) links work correctly
1.
`docs:check:apilint`
: Check that the API docs follow some conventions
1.
`docs
lint`
: Check that all internal (relative) links work correctly and
that all cURL examples in API docs use the full switches.
If your contribution contains
**only**
documentation changes, you can speed up
the CI process by prepending to the name of your branch:
`docs/`
. For example,
a valid name would be
`docs/update-api-issues`
and it will run only the docs
tests. If the name is
`docs-update-api-issues`
, the whole test suite will run
(including docs).
the CI process by following some branch naming conventions. You have three
choices:
| Branch name | Valid example |
| ----------- | ------------- |
| Starting with
`docs/`
|
`docs/update-api-issues`
|
| Starting with
`docs-`
|
`docs-update-api-issues`
|
| Ending in
`-docs`
|
`123-update-api-issues-docs`
|
If your branch name matches any of the above, it will run only the docs
tests. If it doesn't, the whole test suite will run (including docs).
---
...
...
doc/user/project/merge_requests/merge_request_approvals.md
View file @
fb462e2e
...
...
@@ -37,7 +37,7 @@ merge request, they automatically get excluded from the approvers list.
### Approvers
At the approvers area you can
define
the default set of users that need to
At the approvers area you can
select
the default set of users that need to
approve a merge request.
Depending on the number of required approvals and the number of approvers set,
...
...
@@ -56,7 +56,14 @@ creating or editing a merge request.
When someone is marked as a required approver for a merge request, an email is
sent to them and a todo is added to their list of todos.
### Approver groups
### Selecting individual approvers
GitLab restricts the users that can be selected to be individual approvers. Only these can be selected and appear in the search box:
-
Members of the current project
-
Members of the parent group of the current project
-
Members of a group that have access to the current project
[
via a share
](
../../../workflow/share_projects_with_other_groups.md
)
### Selecting group approvers
> [Introduced][ee-743] in GitLab Enterprise Edition 8.13.
...
...
lib/gitlab/etag_caching/router.rb
View file @
fb462e2e
...
...
@@ -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`/re
ndered_title
` for the `issue_title` route
USED_IN_ROUTES
=
%w[noteable issue notes issues re
ndered_title
# - Ending in `issues/id`/re
altime_changes
` for the `issue_title` route
USED_IN_ROUTES
=
%w[noteable issue notes issues re
altime_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
+/re
ndered_title
\z
)
,
%r(^(?!.*(
#{
RESERVED_WORDS_REGEX
}
)).*/issues/
\d
+/re
altime_changes
\z
)
,
'issue_title'
),
Gitlab
::
EtagCaching
::
Router
::
Route
.
new
(
...
...
lib/gitlab/geo.rb
View file @
fb462e2e
...
...
@@ -13,7 +13,7 @@ module Gitlab
)
.
freeze
PRIMARY_JOBS
=
%i(bulk_notify_job)
.
freeze
SECONDARY_JOBS
=
%i(
backfill
_job file_download_job)
.
freeze
SECONDARY_JOBS
=
%i(
repository_sync
_job file_download_job)
.
freeze
def
self
.
current_node
self
.
cache_value
(
:geo_node_current
)
do
...
...
@@ -37,7 +37,7 @@ module Gitlab
def
self
.
current_node_enabled?
# No caching of the enabled! If we cache it and an admin disables
# this node, an active Geo
Backfill
Worker would keep going for up
# this node, an active Geo
RepositorySync
Worker would keep going for up
# to max run time after the node was disabled.
Gitlab
::
Geo
.
current_node
.
reload
.
enabled?
end
...
...
@@ -74,12 +74,12 @@ module Gitlab
Sidekiq
::
Cron
::
Job
.
find
(
'geo_bulk_notify_worker'
)
end
def
self
.
backfill
_job
Sidekiq
::
Cron
::
Job
.
find
(
'geo_
backfill
_worker'
)
def
self
.
repository_sync
_job
Sidekiq
::
Cron
::
Job
.
find
(
'geo_
repository_sync
_worker'
)
end
def
self
.
file_download_job
Sidekiq
::
Cron
::
Job
.
find
(
'geo_download_dispatch_worker'
)
Sidekiq
::
Cron
::
Job
.
find
(
'geo_
file_
download_dispatch_worker'
)
end
def
self
.
configure_primary_jobs!
...
...
spec/features/issues/filtered_search/recent_searches_spec.rb
View file @
fb462e2e
...
...
@@ -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
...
...
spec/features/projects/gfm_autocomplete_load_spec.rb
View file @
fb462e2e
...
...
@@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
it
'does not load on project#show'
do
expect
(
evaluate_script
(
'gl.GfmAutoComplete
.dataSources'
)).
to
eq
({}
)
expect
(
evaluate_script
(
'gl.GfmAutoComplete
'
)).
to
eq
(
nil
)
end
it
'loads on new issue page'
do
...
...
spec/javascripts/gfm_auto_complete_spec.js
View file @
fb462e2e
/* eslint no-param-reassign: "off" */
require
(
'
~/gfm_auto_complete
'
);
import
GfmAutoComplete
from
'
~/gfm_auto_complete
'
;
require
(
'
vendor/jquery.caret
'
);
require
(
'
vendor/jquery.atwho
'
);
const
global
=
window
.
gl
||
(
window
.
gl
=
{});
const
GfmAutoComplete
=
global
.
GfmAutoComplete
;
describe
(
'
GfmAutoComplete
'
,
function
()
{
const
gfmAutoCompleteCallbacks
=
GfmAutoComplete
.
prototype
.
getDefaultCallbacks
.
call
({
fetchData
:
()
=>
{},
});
describe
(
'
DefaultOptions.sorter
'
,
function
()
{
describe
(
'
assets loading
'
,
function
()
{
beforeEach
(
function
()
{
...
...
@@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () {
this
.
atwhoInstance
=
{
setting
:
{}
};
this
.
items
=
[];
this
.
sorterValue
=
GfmAutoComplete
.
DefaultOption
s
.
sorter
this
.
sorterValue
=
gfmAutoCompleteCallback
s
.
sorter
.
call
(
this
.
atwhoInstance
,
''
,
this
.
items
);
});
...
...
@@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () {
it
(
'
should enable highlightFirst if alwaysHighlightFirst is set
'
,
function
()
{
const
atwhoInstance
=
{
setting
:
{
alwaysHighlightFirst
:
true
}
};
GfmAutoComplete
.
DefaultOption
s
.
sorter
.
call
(
atwhoInstance
);
gfmAutoCompleteCallback
s
.
sorter
.
call
(
atwhoInstance
);
expect
(
atwhoInstance
.
setting
.
highlightFirst
).
toBe
(
true
);
});
...
...
@@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () {
it
(
'
should enable highlightFirst if a query is present
'
,
function
()
{
const
atwhoInstance
=
{
setting
:
{}
};
GfmAutoComplete
.
DefaultOption
s
.
sorter
.
call
(
atwhoInstance
,
'
query
'
);
gfmAutoCompleteCallback
s
.
sorter
.
call
(
atwhoInstance
,
'
query
'
);
expect
(
atwhoInstance
.
setting
.
highlightFirst
).
toBe
(
true
);
});
...
...
@@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () {
const
items
=
[];
const
searchKey
=
'
searchKey
'
;
GfmAutoComplete
.
DefaultOption
s
.
sorter
.
call
(
atwhoInstance
,
query
,
items
,
searchKey
);
gfmAutoCompleteCallback
s
.
sorter
.
call
(
atwhoInstance
,
query
,
items
,
searchKey
);
expect
(
$
.
fn
.
atwho
.
default
.
callbacks
.
sorter
).
toHaveBeenCalledWith
(
query
,
items
,
searchKey
);
});
...
...
@@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () {
describe
(
'
DefaultOptions.matcher
'
,
function
()
{
const
defaultMatcher
=
(
context
,
flag
,
subtext
)
=>
(
GfmAutoComplete
.
DefaultOption
s
.
matcher
.
call
(
context
,
flag
,
subtext
)
gfmAutoCompleteCallback
s
.
matcher
.
call
(
context
,
flag
,
subtext
)
);
const
flagsUseDefaultMatcher
=
[
'
@
'
,
'
#
'
,
'
!
'
,
'
~
'
,
'
%
'
];
...
...
spec/javascripts/issue_show/issue_title_description_spec.js
View file @
fb462e2e
...
...
@@ -35,7 +35,7 @@ describe('Issue Title', () => {
const
issueShowComponent
=
new
IssueTitleDescriptionComponent
({
propsData
:
{
canUpdateIssue
:
'
.css-stuff
'
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/re
ndered_title
'
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/re
altime_changes
'
,
},
}).
$mount
();
...
...
spec/javascripts/notes_spec.js
View file @
fb462e2e
...
...
@@ -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
\n
Merging 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
=
{
'
&
'
:
'
&
'
,
'
<
'
:
'
<
'
,
'
>
'
:
'
>
'
,
'
"
'
:
'
"
'
,
"
'
"
:
'
'
'
,
'
`
'
:
'
`
'
}[
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
(
'
<script>alert("Boom!");</script>
'
);
});
it
(
'
should return constructed placeholder element for discussion note based on form contents
'
,
()
=>
{
...
...
spec/lib/gitlab/etag_caching/router_spec.rb
View file @
fb462e2e
...
...
@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do
it
'matches issue title endpoint'
do
env
=
build_env
(
'/my-group/my-project/issues/123/re
ndered_title
'
'/my-group/my-project/issues/123/re
altime_changes
'
)
result
=
described_class
.
match
(
env
)
...
...
spec/lib/gitlab/geo_spec.rb
View file @
fb462e2e
...
...
@@ -122,11 +122,8 @@ describe Gitlab::Geo, lib: true do
end
before
(
:all
)
do
jobs
=
%w(geo_bulk_notify_worker geo_backfill_worker)
jobs
=
%w(geo_bulk_notify_worker geo_repository_sync_worker geo_file_download_dispatch_worker)
jobs
.
each
{
|
job
|
init_cron_job
(
job
,
job
.
camelize
)
}
# TODO: Make this name consistent
init_cron_job
(
'geo_download_dispatch_worker'
,
'GeoFileDownloadDispatchWorker'
)
end
it
'activates cron jobs for primary'
do
...
...
@@ -134,7 +131,7 @@ describe Gitlab::Geo, lib: true do
described_class
.
configure_cron_jobs!
expect
(
described_class
.
bulk_notify_job
).
to
be_enabled
expect
(
described_class
.
backfill
_job
).
not_to
be_enabled
expect
(
described_class
.
repository_sync
_job
).
not_to
be_enabled
expect
(
described_class
.
file_download_job
).
not_to
be_enabled
end
...
...
@@ -143,7 +140,7 @@ describe Gitlab::Geo, lib: true do
described_class
.
configure_cron_jobs!
expect
(
described_class
.
bulk_notify_job
).
not_to
be_enabled
expect
(
described_class
.
backfill
_job
).
to
be_enabled
expect
(
described_class
.
repository_sync
_job
).
to
be_enabled
expect
(
described_class
.
file_download_job
).
to
be_enabled
end
...
...
@@ -151,7 +148,7 @@ describe Gitlab::Geo, lib: true do
described_class
.
configure_cron_jobs!
expect
(
described_class
.
bulk_notify_job
).
not_to
be_enabled
expect
(
described_class
.
backfill
_job
).
not_to
be_enabled
expect
(
described_class
.
repository_sync
_job
).
not_to
be_enabled
expect
(
described_class
.
file_download_job
).
not_to
be_enabled
end
end
...
...
spec/models/user_spec.rb
View file @
fb462e2e
...
...
@@ -1967,4 +1967,36 @@ describe User, models: true do
expect
(
user
.
preferred_language
).
to
eq
(
'en'
)
end
end
describe
'#forget_me!'
do
subject
{
create
(
:user
,
remember_created_at:
Time
.
now
)
}
it
'clears remember_created_at'
do
subject
.
forget_me!
expect
(
subject
.
reload
.
remember_created_at
).
to
be_nil
end
it
'does not clear remember_created_at when in a Geo secondary node'
do
allow
(
Gitlab
::
Geo
).
to
receive
(
:secondary?
)
{
true
}
expect
{
subject
.
forget_me!
}.
not_to
change
(
subject
,
:remember_created_at
)
end
end
describe
'#remember_me!'
do
subject
{
create
(
:user
,
remember_created_at:
nil
)
}
it
'updates remember_created_at'
do
subject
.
remember_me!
expect
(
subject
.
reload
.
remember_created_at
).
not_to
be_nil
end
it
'does not update remember_created_at when in a Geo secondary node'
do
allow
(
Gitlab
::
Geo
).
to
receive
(
:secondary?
)
{
true
}
expect
{
subject
.
remember_me!
}.
not_to
change
(
subject
,
:remember_created_at
)
end
end
end
spec/services/ci/create_pipeline_service_spec.rb
View file @
fb462e2e
...
...
@@ -27,12 +27,14 @@ describe Ci::CreatePipelineService, services: true do
)
end
it
{
expect
(
pipeline
).
to
be_kind_of
(
Ci
::
Pipeline
)
}
it
{
expect
(
pipeline
).
to
be_valid
}
it
{
expect
(
pipeline
).
to
eq
(
project
.
pipelines
.
last
)
}
it
{
expect
(
pipeline
).
to
have_attributes
(
user:
user
)
}
it
{
expect
(
pipeline
).
to
have_attributes
(
status:
'pending'
)
}
it
{
expect
(
pipeline
.
builds
.
first
).
to
be_kind_of
(
Ci
::
Build
)
}
it
'creates a pipeline'
do
expect
(
pipeline
).
to
be_kind_of
(
Ci
::
Pipeline
)
expect
(
pipeline
).
to
be_valid
expect
(
pipeline
).
to
eq
(
project
.
pipelines
.
last
)
expect
(
pipeline
).
to
have_attributes
(
user:
user
)
expect
(
pipeline
).
to
have_attributes
(
status:
'pending'
)
expect
(
pipeline
.
builds
.
first
).
to
be_kind_of
(
Ci
::
Build
)
end
context
'#update_merge_requests_head_pipeline'
do
it
'updates head pipeline of each merge request'
do
...
...
spec/services/geo/repository_
backfill
_service_spec.rb
→
spec/services/geo/repository_
sync
_service_spec.rb
View file @
fb462e2e
require
'spec_helper'
describe
Geo
::
Repository
Backfill
Service
,
services:
true
do
describe
Geo
::
Repository
Sync
Service
,
services:
true
do
let!
(
:primary
)
{
create
(
:geo_node
,
:primary
,
host:
'primary-geo-node'
)
}
subject
{
described_class
.
new
(
project
.
id
)
}
...
...
@@ -106,7 +106,7 @@ describe Geo::RepositoryBackfillService, services: true do
end
end
context
'when repository was
backfill
ed successfully'
do
context
'when repository was
sync
ed successfully'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:last_repository_synced_at
)
{
5
.
days
.
ago
}
...
...
@@ -159,7 +159,7 @@ describe Geo::RepositoryBackfillService, services: true do
end
end
context
'when last attempt to
backfill
the repository failed'
do
context
'when last attempt to
sync
the repository failed'
do
let
(
:project
)
{
create
(
:project
)
}
let!
(
:registry
)
do
...
...
spec/services/issues/close_service_spec.rb
View file @
fb462e2e
...
...
@@ -51,8 +51,10 @@ describe Issues::CloseService, services: true do
end
end
it
{
expect
(
issue
).
to
be_valid
}
it
{
expect
(
issue
).
to
be_closed
}
it
'closes the issue'
do
expect
(
issue
).
to
be_valid
expect
(
issue
).
to
be_closed
end
it
'sends email to user2 about assign of new issue'
do
email
=
ActionMailer
::
Base
.
deliveries
.
last
...
...
@@ -96,9 +98,11 @@ describe Issues::CloseService, services: true do
described_class
.
new
(
project
,
user
).
close_issue
(
issue
)
end
it
{
expect
(
issue
).
to
be_valid
}
it
{
expect
(
issue
).
to
be_opened
}
it
{
expect
(
todo
.
reload
).
to
be_pending
}
it
'closes the issue'
do
expect
(
issue
).
to
be_valid
expect
(
issue
).
to
be_opened
expect
(
todo
.
reload
).
to
be_pending
end
end
end
end
spec/services/merge_requests/create_service_spec.rb
View file @
fb462e2e
...
...
@@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do
@merge_request
=
service
.
execute
end
it
{
expect
(
@merge_request
).
to
be_valid
}
it
{
expect
(
@merge_request
.
title
).
to
eq
(
'Awesome merge_request'
)
}
it
{
expect
(
@merge_request
.
assignee
).
to
be_nil
}
it
{
expect
(
@merge_request
.
merge_params
[
'force_remove_source_branch'
]).
to
eq
(
'1'
)
}
it
'creates an MR'
do
expect
(
@merge_request
).
to
be_valid
expect
(
@merge_request
.
title
).
to
eq
(
'Awesome merge_request'
)
expect
(
@merge_request
.
assignee
).
to
be_nil
expect
(
@merge_request
.
merge_params
[
'force_remove_source_branch'
]).
to
eq
(
'1'
)
end
it
'executes hooks with default action'
do
expect
(
service
).
to
have_received
(
:execute_hooks
).
with
(
@merge_request
)
...
...
spec/services/merge_requests/update_service_spec.rb
View file @
fb462e2e
...
...
@@ -59,14 +59,16 @@ describe MergeRequests::UpdateService, services: true do
end
end
it
{
expect
(
@merge_request
).
to
be_valid
}
it
{
expect
(
@merge_request
.
title
).
to
eq
(
'New title'
)
}
it
{
expect
(
@merge_request
.
assignee
).
to
eq
(
user2
)
}
it
{
expect
(
@merge_request
).
to
be_closed
}
it
{
expect
(
@merge_request
.
labels
.
count
).
to
eq
(
1
)
}
it
{
expect
(
@merge_request
.
labels
.
first
.
title
).
to
eq
(
label
.
name
)
}
it
{
expect
(
@merge_request
.
target_branch
).
to
eq
(
'target'
)
}
it
{
expect
(
@merge_request
.
merge_params
[
'force_remove_source_branch'
]).
to
eq
(
'1'
)
}
it
'mathces base expectations'
do
expect
(
@merge_request
).
to
be_valid
expect
(
@merge_request
.
title
).
to
eq
(
'New title'
)
expect
(
@merge_request
.
assignee
).
to
eq
(
user2
)
expect
(
@merge_request
).
to
be_closed
expect
(
@merge_request
.
labels
.
count
).
to
eq
(
1
)
expect
(
@merge_request
.
labels
.
first
.
title
).
to
eq
(
label
.
name
)
expect
(
@merge_request
.
target_branch
).
to
eq
(
'target'
)
expect
(
@merge_request
.
merge_params
[
'force_remove_source_branch'
]).
to
eq
(
'1'
)
end
it
'executes hooks with update action'
do
expect
(
service
).
to
have_received
(
:execute_hooks
).
...
...
@@ -148,9 +150,11 @@ describe MergeRequests::UpdateService, services: true do
end
end
it
{
expect
(
@merge_request
).
to
be_valid
}
it
{
expect
(
@merge_request
.
state
).
to
eq
(
'merged'
)
}
it
{
expect
(
@merge_request
.
merge_error
).
to
be_nil
}
it
'merges the MR'
do
expect
(
@merge_request
).
to
be_valid
expect
(
@merge_request
.
state
).
to
eq
(
'merged'
)
expect
(
@merge_request
.
merge_error
).
to
be_nil
end
end
context
'with finished pipeline'
do
...
...
@@ -167,8 +171,10 @@ describe MergeRequests::UpdateService, services: true do
end
end
it
{
expect
(
@merge_request
).
to
be_valid
}
it
{
expect
(
@merge_request
.
state
).
to
eq
(
'merged'
)
}
it
'merges the MR'
do
expect
(
@merge_request
).
to
be_valid
expect
(
@merge_request
.
state
).
to
eq
(
'merged'
)
end
end
context
'with active pipeline'
do
...
...
@@ -202,8 +208,10 @@ describe MergeRequests::UpdateService, services: true do
end
end
it
{
expect
(
@merge_request
.
state
).
to
eq
(
'opened'
)
}
it
{
expect
(
@merge_request
.
merge_error
).
not_to
be_nil
}
it
'does not merge the MR'
do
expect
(
@merge_request
.
state
).
to
eq
(
'opened'
)
expect
(
@merge_request
.
merge_error
).
not_to
be_nil
end
end
context
'MR can not be merged when note sha != MR sha'
do
...
...
spec/support/filtered_search_helpers.rb
View file @
fb462e2e
...
...
@@ -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
)
...
...
spec/workers/geo/geo_
backfill
_worker_spec.rb
→
spec/workers/geo/geo_
repository_sync
_worker_spec.rb
View file @
fb462e2e
require
'spec_helper'
describe
Geo
::
Geo
Backfill
Worker
,
services:
true
do
describe
Geo
::
Geo
RepositorySync
Worker
,
services:
true
do
let!
(
:primary
)
{
create
(
:geo_node
,
:primary
,
host:
'primary-geo-node'
)
}
let!
(
:secondary
)
{
create
(
:geo_node
,
:current
)
}
let!
(
:project_1
)
{
create
(
:empty_project
)
}
...
...
@@ -13,25 +13,25 @@ describe Geo::GeoBackfillWorker, services: true do
allow_any_instance_of
(
Gitlab
::
ExclusiveLease
).
to
receive
(
:try_obtain
)
{
true
}
end
it
'performs Geo::Repository
Backfill
Service for each project'
do
expect
(
Geo
::
Repository
Backfill
Service
).
to
receive
(
:new
).
twice
.
and_return
(
spy
)
it
'performs Geo::Repository
Sync
Service for each project'
do
expect
(
Geo
::
Repository
Sync
Service
).
to
receive
(
:new
).
twice
.
and_return
(
spy
)
subject
.
perform
end
it
'performs Geo::Repository
BackfillService for projects where last attempt to backfill
failed'
do
it
'performs Geo::Repository
SyncService for projects where last attempt to sync
failed'
do
Geo
::
ProjectRegistry
.
create
(
project:
project_1
,
last_repository_synced_at:
DateTime
.
now
,
last_repository_successful_sync_at:
nil
)
expect
(
Geo
::
Repository
Backfill
Service
).
to
receive
(
:new
).
twice
.
and_return
(
spy
)
expect
(
Geo
::
Repository
Sync
Service
).
to
receive
(
:new
).
twice
.
and_return
(
spy
)
subject
.
perform
end
it
'performs Geo::Repository
BackfillService for backfill
ed projects updated recently'
do
it
'performs Geo::Repository
SyncService for sync
ed projects updated recently'
do
Geo
::
ProjectRegistry
.
create
(
project:
project_1
,
last_repository_synced_at:
2
.
days
.
ago
,
...
...
@@ -47,39 +47,39 @@ describe Geo::GeoBackfillWorker, services: true do
project_1
.
update_attribute
(
:last_repository_updated_at
,
2
.
days
.
ago
)
project_2
.
update_attribute
(
:last_repository_updated_at
,
10
.
minutes
.
ago
)
expect
(
Geo
::
Repository
Backfill
Service
).
to
receive
(
:new
).
once
.
and_return
(
spy
)
expect
(
Geo
::
Repository
Sync
Service
).
to
receive
(
:new
).
once
.
and_return
(
spy
)
subject
.
perform
end
it
'does not perform Geo::Repository
Backfill
Service when tracking DB is not available'
do
it
'does not perform Geo::Repository
Sync
Service when tracking DB is not available'
do
allow
(
Rails
.
configuration
).
to
receive
(
:respond_to?
).
with
(
:geo_database
)
{
false
}
expect
(
Geo
::
Repository
Backfill
Service
).
not_to
receive
(
:new
)
expect
(
Geo
::
Repository
Sync
Service
).
not_to
receive
(
:new
)
subject
.
perform
end
it
'does not perform Geo::Repository
Backfill
Service when primary node does not exists'
do
it
'does not perform Geo::Repository
Sync
Service when primary node does not exists'
do
allow
(
Gitlab
::
Geo
).
to
receive
(
:primary_node
)
{
nil
}
expect
(
Geo
::
Repository
Backfill
Service
).
not_to
receive
(
:new
)
expect
(
Geo
::
Repository
Sync
Service
).
not_to
receive
(
:new
)
subject
.
perform
end
it
'does not perform Geo::Repository
Backfill
Service when node is disabled'
do
it
'does not perform Geo::Repository
Sync
Service when node is disabled'
do
allow_any_instance_of
(
GeoNode
).
to
receive
(
:enabled?
)
{
false
}
expect
(
Geo
::
Repository
Backfill
Service
).
not_to
receive
(
:new
)
expect
(
Geo
::
Repository
Sync
Service
).
not_to
receive
(
:new
)
subject
.
perform
end
it
'does not perform Geo::Repository
Backfill
Service when can not obtain a lease'
do
it
'does not perform Geo::Repository
Sync
Service when can not obtain a lease'
do
allow_any_instance_of
(
Gitlab
::
ExclusiveLease
).
to
receive
(
:try_obtain
)
{
false
}
expect
(
Geo
::
Repository
Backfill
Service
).
not_to
receive
(
:new
)
expect
(
Geo
::
Repository
Sync
Service
).
not_to
receive
(
:new
)
subject
.
perform
end
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment