Merge branch 'master' into 34312-eslint-vue-plugin
* master: (140 commits) Add Gitter room link to I want to contribute since you always have questions Use workhorse 3.4.0 chore: remove symbolic link Add memoization for properties Resolve "Allow QA tests to run with `CHROME_HEADLESS=false`" Resolve "Add graph value to hover" Fix slash commands dropdown description disables the shortcut to the issue boards when issues are disabled Fix static analysys Disable STI of ActiveRecord. Refactoring specs. Fix StaticSnalysys Fix change log Add changelog Revert bulk_insert and bring back AR insert(one by one) Add a new test for emptified params Use batch update for Service deactivation Fix query to look for proper unmanaged kubernetes service Fix static anylysy Use bulk_insert instead of AR create Opitmize migration process by using both unmanaged_kubernetes_service and kubernetes_service_without_template ...
This commit is contained in:
commit
e2b759a2f4
|
@ -104,9 +104,13 @@ the remaining issues on the GitHub issue tracker.
|
|||
|
||||
## I want to contribute!
|
||||
|
||||
If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
|
||||
These issues will be of reasonable size and challenge, for anyone to start
|
||||
contributing to GitLab.
|
||||
If you want to contribute to GitLab [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight]
|
||||
is a great place to start. Issues with a lower weight (1 or 2) are deemed
|
||||
suitable for beginners. These issues will be of reasonable size and challenge,
|
||||
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
|
||||
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
|
||||
please consider we favor
|
||||
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
|
||||
|
||||
## Workflow labels
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.65.0
|
||||
0.66.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.3.1
|
||||
3.4.0
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -78,7 +78,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist'
|
|||
# API
|
||||
gem 'grape', '~> 1.0'
|
||||
gem 'grape-entity', '~> 0.6.0'
|
||||
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
|
||||
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
|
||||
|
||||
# Disable strong_params so that Mash does not respond to :permitted?
|
||||
gem 'hashie-forbidden_attributes'
|
||||
|
@ -339,7 +339,7 @@ group :development, :test do
|
|||
gem 'rubocop', '~> 0.52.0'
|
||||
gem 'rubocop-rspec', '~> 1.20.1'
|
||||
|
||||
gem 'scss_lint', '~> 0.54.0', require: false
|
||||
gem 'scss_lint', '~> 0.56.0', require: false
|
||||
gem 'haml_lint', '~> 0.26.0', require: false
|
||||
gem 'simplecov', '~> 0.14.0', require: false
|
||||
gem 'flay', '~> 2.8.0', require: false
|
||||
|
|
21
Gemfile.lock
21
Gemfile.lock
|
@ -651,7 +651,7 @@ GEM
|
|||
rack (>= 0.4)
|
||||
rack-attack (4.4.1)
|
||||
rack
|
||||
rack-cors (0.4.0)
|
||||
rack-cors (1.0.2)
|
||||
rack-oauth2 (1.2.3)
|
||||
activesupport (>= 2.3)
|
||||
attr_required (>= 0.0.5)
|
||||
|
@ -695,6 +695,9 @@ GEM
|
|||
rake
|
||||
raindrops (0.18.0)
|
||||
rake (12.3.0)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rblineprof (0.3.6)
|
||||
debugger-ruby_core_source (~> 1.3)
|
||||
rbnacl (4.0.2)
|
||||
|
@ -809,7 +812,11 @@ GEM
|
|||
safe_yaml (1.0.4)
|
||||
sanitize (2.1.0)
|
||||
nokogiri (>= 1.4.4)
|
||||
sass (3.4.22)
|
||||
sass (3.5.5)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sass-rails (5.0.6)
|
||||
railties (>= 4.0.0, < 6)
|
||||
sass (~> 3.1)
|
||||
|
@ -819,9 +826,9 @@ GEM
|
|||
sawyer (0.8.1)
|
||||
addressable (>= 2.3.5, < 2.6)
|
||||
faraday (~> 0.8, < 1.0)
|
||||
scss_lint (0.54.0)
|
||||
scss_lint (0.56.0)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.4.20)
|
||||
sass (~> 3.5.3)
|
||||
securecompare (1.0.0)
|
||||
seed-fu (2.3.6)
|
||||
activerecord (>= 3.1)
|
||||
|
@ -1126,7 +1133,7 @@ DEPENDENCIES
|
|||
pry-byebug (~> 3.4.1)
|
||||
pry-rails (~> 0.3.4)
|
||||
rack-attack (~> 4.4.1)
|
||||
rack-cors (~> 0.4.0)
|
||||
rack-cors (~> 1.0.0)
|
||||
rack-oauth2 (~> 1.2.1)
|
||||
rack-proxy (~> 0.6.0)
|
||||
rails (= 4.2.10)
|
||||
|
@ -1162,7 +1169,7 @@ DEPENDENCIES
|
|||
rugged (~> 0.26.0)
|
||||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.6)
|
||||
scss_lint (~> 0.54.0)
|
||||
scss_lint (~> 0.56.0)
|
||||
seed-fu (= 2.3.6)
|
||||
select2-rails (~> 3.5.9)
|
||||
selenium-webdriver (~> 3.5)
|
||||
|
@ -1205,4 +1212,4 @@ DEPENDENCIES
|
|||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.0
|
||||
1.16.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"iconCount":186,"spriteSize":84748,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
|
||||
{"iconCount":189,"spriteSize":85766,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o-open","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300"><g fill="none" fill-rule="evenodd" transform="translate(35 29)"><path fill="#EEE" fill-rule="nonzero" d="M90 23a2 2 0 1 1 0-4h10a2 2 0 0 1 0 4H90zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h10a2 2 0 0 1 0 4h-10zm20 0a2 2 0 0 1 0-4h1a11.98 11.98 0 0 1 9.457 4.612 2 2 0 0 1-3.151 2.464A7.981 7.981 0 0 0 331 23h-1zm9 11.39a2 2 0 0 1 4 0v10a2 2 0 0 1-4 0v-10zm0 180a2 2 0 1 1 4 0V223c0 .56-.038 1.114-.114 1.662a2 2 0 0 1-3.962-.55A8.21 8.21 0 0 0 339 223v-8.61zm-4.769 15.931a2 2 0 0 1 1.618 3.658A11.967 11.967 0 0 1 331 235h-5.782a2 2 0 0 1 0-4H331c1.13 0 2.224-.233 3.231-.679zm-19.013.679a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zm-20 0a2 2 0 1 1 0 4h-10a2 2 0 0 1 0-4h10zM115 231a2 2 0 0 1 0 4h-10a2 2 0 0 1 0-4h10zm-26.2 4c.131-.646.2-1.315.2-2v-2h4a2 2 0 0 1 0 4h-4.2z"/><path fill="#EEE" fill-rule="nonzero" d="M103 211h258a6 6 0 0 0 6-6V63a6 6 0 0 0-6-6H166a5 5 0 0 1-5-5v-8.5a5.5 5.5 0 0 0-5.5-5.5H109a6 6 0 0 0-6 6v167zm62-167.5V52a1 1 0 0 0 1 1h195c5.523 0 10 4.477 10 10v142c0 5.523-4.477 10-10 10H99V44c0-5.523 4.477-10 10-10h46.5a9.5 9.5 0 0 1 9.5 9.5z"/><rect width="40" height="4" x="118" y="78" fill="#6B4FBB" rx="2"/><rect width="30" height="4" x="118" y="90" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="153" y="90" fill="#E1DBF1" rx="2"/><rect width="150" height="4" x="118" y="102" fill="#EFEDF8" rx="2"/><rect width="90" height="4" x="118" y="114" fill="#E1DBF1" rx="2"/><rect width="60" height="4" x="118" y="138" fill="#EFEDF8" rx="2"/><rect width="20" height="4" x="118" y="150" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="144" y="150" fill="#C3B8E3" rx="2"/><rect width="20" height="4" x="170" y="150" fill="#E1DBF1" rx="2"/><rect width="130" height="4" x="118" y="162" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="118" y="174" fill="#C3B8E3" rx="2"/><rect width="30" height="4" x="154" y="174" fill="#EFEDF8" rx="2"/><rect width="30" height="4" x="190" y="174" fill="#EFEDF8" rx="2"/><rect width="40" height="4" x="118" y="186" fill="#E1DBF1" rx="2"/><path fill="#F9F9F9" d="M89 24.292l11.434 19.326v170.326L89 226.336V24.292z"/><path fill="#EEE" fill-rule="nonzero" d="M89 229.286v-5.9l9.434-10.223V44.165L89 28.22v-7.856l13.434 22.707v171.655L89 229.286zM10 4a6 6 0 0 0-6 6v223a6 6 0 0 0 6 6h69a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h69c5.523 0 10 4.477 10 10v223c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><circle cx="25" cy="23" r="11" fill="#FEF0E8"/><path fill="#FEE1D3" d="M46 17h16a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4zm0 8h27a2 2 0 1 1 0 4H46a2 2 0 1 1 0-4z"/><path fill="#EEE" d="M16 50h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-4 12h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4zM26 78h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H40a2 2 0 1 1 0-4z"/><g transform="translate(14 110)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><path fill="#EEE" d="M16 140h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4zm-14 14h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm14 2h24a2 2 0 1 1 0 4H30a2 2 0 1 1 0-4z"/><g transform="translate(24 124)"><rect width="8" height="8" fill="#FEE1D3" rx="2"/><rect width="28" height="4" x="14" y="2" fill="#FEF0E8" rx="2"/></g><g fill="#FC6D26" transform="translate(24 92)"><rect width="8" height="8" rx="2"/><rect width="28" height="4" x="14" y="2" rx="2"/></g><path fill="#FDC4A8" fill-rule="nonzero" d="M152 50.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zm0-3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/></g></svg>
|
After Width: | Height: | Size: 4.6 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.9 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.0 KiB |
|
@ -57,12 +57,12 @@ class GfmAutoComplete {
|
|||
displayTpl(value) {
|
||||
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
let tpl = '<li>/${name}';
|
||||
let tpl = '<li><span class="name">/${name}</span>';
|
||||
if (value.aliases.length > 0) {
|
||||
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
|
||||
tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>';
|
||||
}
|
||||
if (value.params.length > 0) {
|
||||
tpl += ' <small><%- params.join(" ") %></small>';
|
||||
tpl += ' <small class="params"><%- params.join(" ") %></small>';
|
||||
}
|
||||
if (value.description !== '') {
|
||||
tpl += '<small class="description"><i><%- description %></i></small>';
|
||||
|
|
|
@ -84,11 +84,14 @@
|
|||
:group="group"
|
||||
:parent-group="parentGroup"
|
||||
/>
|
||||
<item-stats :item="group" />
|
||||
<item-stats
|
||||
:item="group"
|
||||
/>
|
||||
<div
|
||||
class="folder-toggle-wrap"
|
||||
>
|
||||
<item-caret :is-group-open="group.isOpen" />
|
||||
class="folder-toggle-wrap">
|
||||
<item-caret
|
||||
:is-group-open="group.isOpen"
|
||||
/>
|
||||
<item-type-icon
|
||||
:item-type="group.type"
|
||||
:is-group-open="group.isOpen"
|
||||
|
@ -110,14 +113,13 @@
|
|||
<identicon
|
||||
v-else
|
||||
size-class="s24"
|
||||
:entity-id="group.id"
|
||||
:entity-id=group.id
|
||||
:entity-name="group.name"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="title namespace-title"
|
||||
>
|
||||
class="title namespace-title">
|
||||
<a
|
||||
v-tooltip
|
||||
:href="group.relativePath"
|
||||
|
@ -139,7 +141,8 @@
|
|||
<div
|
||||
v-if="group.description"
|
||||
class="description">
|
||||
{{ group.description }}
|
||||
<span v-html="group.description">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<group-folder
|
||||
|
|
|
@ -71,7 +71,7 @@ export default class GroupsStore {
|
|||
id: rawGroupItem.id,
|
||||
name: rawGroupItem.name,
|
||||
fullName: rawGroupItem.full_name,
|
||||
description: rawGroupItem.description,
|
||||
description: rawGroupItem.markdown_description,
|
||||
visibility: rawGroupItem.visibility,
|
||||
avatarUrl: rawGroupItem.avatar_url,
|
||||
relativePath: rawGroupItem.relative_path,
|
||||
|
|
|
@ -9,14 +9,11 @@ import repoPreview from './repo_preview.vue';
|
|||
import repoEditor from './repo_editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
repoFileButtons,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
repoPreview,
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
|
@ -28,6 +25,15 @@ export default {
|
|||
'activeFile',
|
||||
]),
|
||||
},
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
repoFileButtons,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
repoPreview,
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = (e) => {
|
||||
|
@ -51,8 +57,7 @@ export default {
|
|||
class="multi-file-edit-pane"
|
||||
>
|
||||
<template
|
||||
v-if="activeFile"
|
||||
>
|
||||
v-if="activeFile">
|
||||
<repo-tabs/>
|
||||
<component
|
||||
class="multi-file-edit-pane-content"
|
||||
|
@ -60,14 +65,28 @@ export default {
|
|||
/>
|
||||
<repo-file-buttons/>
|
||||
<ide-status-bar
|
||||
:file="selectedFile"
|
||||
/>
|
||||
:file="selectedFile"/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
v-else>
|
||||
<div class="ide-empty-state">
|
||||
<h2 class="clgray">Welcome to the GitLab IDE</h2>
|
||||
<div class="row js-empty-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content svg-250">
|
||||
<img :src="emptyStateSvgPath">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="text-content text-center">
|
||||
<h4>
|
||||
Welcome to the GitLab IDE
|
||||
</h4>
|
||||
<p>
|
||||
You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import RepoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
import RepoLoadingFile from './repo_loading_file.vue';
|
||||
import repoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import repoFile from './repo_file.vue';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
import { treeList } from '../stores/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'repo-previous-directory': RepoPreviousDirectory,
|
||||
'repo-file': RepoFile,
|
||||
'repo-loading-file': RepoLoadingFile,
|
||||
repoPreviousDirectory,
|
||||
repoFile,
|
||||
skeletonLoadingContainer,
|
||||
},
|
||||
props: {
|
||||
treeId: {
|
||||
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'trees',
|
||||
'isRoot',
|
||||
]),
|
||||
...mapState({
|
||||
|
@ -34,7 +34,10 @@
|
|||
return !this.isRoot && this.fetchedList.length;
|
||||
},
|
||||
showLoading() {
|
||||
return this.loading;
|
||||
if (this.trees[this.treeId]) {
|
||||
return this.trees[this.treeId].loading;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -45,17 +48,17 @@
|
|||
<div class="ide-file-list">
|
||||
<table class="table">
|
||||
<tbody
|
||||
v-if="treeId"
|
||||
>
|
||||
v-if="treeId">
|
||||
<repo-previous-directory
|
||||
v-if="hasPreviousDirectory"
|
||||
/>
|
||||
<template v-if="showLoading">
|
||||
<repo-loading-file
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-if="showLoading"
|
||||
v-for="n in 3"
|
||||
:key="n">
|
||||
<skeleton-loading-container/>
|
||||
</div>
|
||||
<repo-file
|
||||
v-for="file in fetchedList"
|
||||
:key="file.key"
|
||||
|
|
|
@ -3,12 +3,14 @@ import { mapState, mapActions } from 'vuex';
|
|||
import projectTree from './ide_project_tree.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
projectTree,
|
||||
icon,
|
||||
panelResizer,
|
||||
skeletonLoadingContainer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -17,6 +19,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'projects',
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
|
@ -32,6 +35,9 @@ export default {
|
|||
}
|
||||
return {};
|
||||
},
|
||||
showLoading() {
|
||||
return this.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
|
@ -63,6 +69,13 @@ export default {
|
|||
:style="panelStyle"
|
||||
>
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-if="showLoading"
|
||||
v-for="n in 3"
|
||||
:key="n">
|
||||
<skeleton-loading-container/>
|
||||
</div>
|
||||
<project-tree
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
|
|
|
@ -8,9 +8,11 @@ export const getProjectData = (
|
|||
{ namespace, projectId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if (!state.projects[`${namespace}/${projectId}`] || force) {
|
||||
commit(types.TOGGLE_LOADING, state);
|
||||
service.getProjectData(namespace, projectId)
|
||||
.then(res => res.data)
|
||||
.then((data) => {
|
||||
commit(types.TOGGLE_LOADING, state);
|
||||
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
|
||||
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
|
||||
resolve(data);
|
||||
|
|
|
@ -30,6 +30,9 @@
|
|||
shouldRenderContent() {
|
||||
return !this.isLoading && Object.keys(this.job).length;
|
||||
},
|
||||
jobStarted() {
|
||||
return this.job.started;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
job() {
|
||||
|
@ -64,6 +67,7 @@
|
|||
:user="job.user"
|
||||
:actions="actions"
|
||||
:has-sidebar-button="true"
|
||||
:should-render-triggered-label="jobStarted"
|
||||
/>
|
||||
<loading-icon
|
||||
v-if="isLoading"
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import { max, extent } from 'd3-array';
|
||||
import { select } from 'd3-selection';
|
||||
import graphLegend from './graph/legend.vue';
|
||||
import graphFlag from './graph/flag.vue';
|
||||
import graphDeployment from './graph/deployment.vue';
|
||||
import graphPath from './graph/path.vue';
|
||||
import GraphLegend from './graph/legend.vue';
|
||||
import GraphFlag from './graph/flag.vue';
|
||||
import GraphDeployment from './graph/deployment.vue';
|
||||
import GraphPath from './graph/path.vue';
|
||||
import MonitoringMixin from '../mixins/monitoring_mixins';
|
||||
import eventHub from '../event_hub';
|
||||
import measurements from '../utils/measurements';
|
||||
|
@ -17,15 +17,6 @@
|
|||
const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
|
||||
|
||||
export default {
|
||||
components: {
|
||||
graphLegend,
|
||||
graphFlag,
|
||||
graphDeployment,
|
||||
graphPath,
|
||||
},
|
||||
|
||||
mixins: [MonitoringMixin],
|
||||
|
||||
props: {
|
||||
graphData: {
|
||||
type: Object,
|
||||
|
@ -54,6 +45,8 @@
|
|||
},
|
||||
},
|
||||
|
||||
mixins: [MonitoringMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
baseGraphHeight: 450,
|
||||
|
@ -76,21 +69,25 @@
|
|||
currentFlagPosition: 0,
|
||||
showFlag: false,
|
||||
showFlagContent: false,
|
||||
showDeployInfo: true,
|
||||
timeSeries: [],
|
||||
realPixelRatio: 1,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
GraphLegend,
|
||||
GraphFlag,
|
||||
GraphDeployment,
|
||||
GraphPath,
|
||||
},
|
||||
|
||||
computed: {
|
||||
outerViewBox() {
|
||||
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
|
||||
},
|
||||
|
||||
innerViewBox() {
|
||||
if ((this.baseGraphWidth - 150) > 0) {
|
||||
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
|
||||
}
|
||||
return '0 0 0 0';
|
||||
},
|
||||
|
||||
axisTransform() {
|
||||
|
@ -102,26 +99,10 @@
|
|||
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
updateAspectRatio() {
|
||||
if (this.updateAspectRatio) {
|
||||
this.graphHeight = 450;
|
||||
this.graphWidth = 600;
|
||||
this.measurements = measurements.large;
|
||||
this.draw();
|
||||
eventHub.$emit('toggleAspectRatio');
|
||||
}
|
||||
deploymentFlagData() {
|
||||
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
|
||||
},
|
||||
|
||||
hoverData() {
|
||||
this.positionFlag();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.draw();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -142,6 +123,10 @@
|
|||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||
this.baseGraphHeight = this.graphHeight;
|
||||
this.baseGraphWidth = this.graphWidth;
|
||||
|
||||
// pixel offsets inside the svg and outside are not 1:1
|
||||
this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
|
||||
|
||||
this.renderAxesPaths();
|
||||
this.formatDeployments();
|
||||
},
|
||||
|
@ -212,6 +197,26 @@
|
|||
}); // This will select all of the ticks once they're rendered
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
updateAspectRatio() {
|
||||
if (this.updateAspectRatio) {
|
||||
this.graphHeight = 450;
|
||||
this.graphWidth = 600;
|
||||
this.measurements = measurements.large;
|
||||
this.draw();
|
||||
eventHub.$emit('toggleAspectRatio');
|
||||
}
|
||||
},
|
||||
|
||||
hoverData() {
|
||||
this.positionFlag();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.draw();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -231,12 +236,12 @@
|
|||
ref="baseSvg">
|
||||
<g
|
||||
class="x-axis"
|
||||
:transform="axisTransform"
|
||||
/>
|
||||
:transform="axisTransform">
|
||||
</g>
|
||||
<g
|
||||
class="y-axis"
|
||||
transform="translate(70, 20)"
|
||||
/>
|
||||
transform="translate(70, 20)">
|
||||
</g>
|
||||
<graph-legend
|
||||
:graph-width="graphWidth"
|
||||
:graph-height="graphHeight"
|
||||
|
@ -251,8 +256,7 @@
|
|||
<svg
|
||||
class="graph-data"
|
||||
:viewBox="innerViewBox"
|
||||
ref="graphData"
|
||||
>
|
||||
ref="graphData">
|
||||
<graph-path
|
||||
v-for="(path, index) in timeSeries"
|
||||
:key="index"
|
||||
|
@ -262,32 +266,34 @@
|
|||
:line-color="path.lineColor"
|
||||
:area-color="path.areaColor"
|
||||
/>
|
||||
<graph-deployment
|
||||
:deployment-data="reducedDeploymentData"
|
||||
:graph-height="graphHeight"
|
||||
:graph-height-offset="graphHeightOffset"
|
||||
/>
|
||||
<rect
|
||||
class="prometheus-graph-overlay"
|
||||
:width="(graphWidth - 70)"
|
||||
:height="(graphHeight - 100)"
|
||||
transform="translate(-5, 20)"
|
||||
ref="graphOverlay"
|
||||
@mousemove="handleMouseOverGraph($event)"
|
||||
/>
|
||||
<graph-deployment
|
||||
:show-deploy-info="showDeployInfo"
|
||||
:deployment-data="reducedDeploymentData"
|
||||
:graph-width="graphWidth"
|
||||
:graph-height="graphHeight"
|
||||
:graph-height-offset="graphHeightOffset"
|
||||
/>
|
||||
@mousemove="handleMouseOverGraph($event)">
|
||||
</rect>
|
||||
</svg>
|
||||
</svg>
|
||||
<graph-flag
|
||||
v-if="showFlag"
|
||||
:real-pixel-ratio="realPixelRatio"
|
||||
:current-x-coordinate="currentXCoordinate"
|
||||
:current-data="currentData"
|
||||
:current-flag-position="currentFlagPosition"
|
||||
:graph-height="graphHeight"
|
||||
:graph-height-offset="graphHeightOffset"
|
||||
:show-flag-content="showFlagContent"
|
||||
:time-series="timeSeries"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
:current-data-index="currentDataIndex"
|
||||
:legend-title="legendTitle"
|
||||
:deployment-flag-data="deploymentFlagData"
|
||||
/>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
<script>
|
||||
import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
showDeployInfo: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
deploymentData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
@ -23,10 +13,6 @@
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -36,165 +22,50 @@
|
|||
},
|
||||
|
||||
methods: {
|
||||
refText(d) {
|
||||
return d.tag ? d.ref : d.sha.slice(0, 8);
|
||||
},
|
||||
|
||||
formatTime(deploymentTime) {
|
||||
return timeFormat(deploymentTime);
|
||||
},
|
||||
|
||||
formatDate(deploymentTime) {
|
||||
return dateFormatWithName(deploymentTime);
|
||||
},
|
||||
|
||||
nameDeploymentClass(deployment) {
|
||||
return `deploy-info-${deployment.id}`;
|
||||
},
|
||||
|
||||
transformDeploymentGroup(deployment) {
|
||||
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
|
||||
},
|
||||
|
||||
positionFlag(deployment) {
|
||||
let xPosition = 3;
|
||||
if (deployment.xPos > (this.graphWidth - 225)) {
|
||||
xPosition = -142;
|
||||
}
|
||||
return xPosition;
|
||||
},
|
||||
|
||||
svgContainerHeight(tag) {
|
||||
let svgHeight = 80;
|
||||
if (!tag) {
|
||||
svgHeight -= 20;
|
||||
}
|
||||
return svgHeight;
|
||||
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<g
|
||||
class="deploy-info"
|
||||
v-if="showDeployInfo">
|
||||
<g class="deploy-info">
|
||||
<g
|
||||
v-for="(deployment, index) in deploymentData"
|
||||
:key="index"
|
||||
:class="nameDeploymentClass(deployment)"
|
||||
:transform="transformDeploymentGroup(deployment)"
|
||||
>
|
||||
:transform="transformDeploymentGroup(deployment)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
:height="calculatedHeight"
|
||||
width="3"
|
||||
fill="url(#shadow-gradient)"
|
||||
/>
|
||||
fill="url(#shadow-gradient)">
|
||||
</rect>
|
||||
<line
|
||||
class="deployment-line"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
:y2="calculatedHeight"
|
||||
stroke="#000"
|
||||
/>
|
||||
<svg
|
||||
v-if="deployment.showDeploymentFlag"
|
||||
class="js-deploy-info-box"
|
||||
:x="positionFlag(deployment)"
|
||||
y="0"
|
||||
width="134"
|
||||
:height="svgContainerHeight(deployment.tag)"
|
||||
>
|
||||
<rect
|
||||
class="rect-text-metric deploy-info-rect rect-metric"
|
||||
x="1"
|
||||
y="1"
|
||||
rx="2"
|
||||
width="132"
|
||||
:height="svgContainerHeight(deployment.tag) - 2"
|
||||
/>
|
||||
<text
|
||||
class="deploy-info-text text-metric-bold"
|
||||
transform="translate(5, 2)"
|
||||
>
|
||||
Deployed
|
||||
</text>
|
||||
<!--The date info-->
|
||||
<g transform="translate(5, 20)">
|
||||
<text class="deploy-info-text">
|
||||
{{ formatDate(deployment.time) }}
|
||||
</text>
|
||||
<text
|
||||
class="deploy-info-text text-metric-bold"
|
||||
x="62"
|
||||
>
|
||||
{{ formatTime(deployment.time) }}
|
||||
</text>
|
||||
</g>
|
||||
<line
|
||||
class="divider-line"
|
||||
x1="0"
|
||||
y1="38"
|
||||
x2="132"
|
||||
:y2="38"
|
||||
stroke="#000"
|
||||
/>
|
||||
<!--Commit information-->
|
||||
<g transform="translate(5, 40)">
|
||||
<icon
|
||||
name="commit"
|
||||
:width="12"
|
||||
:height="12"
|
||||
:y="3"
|
||||
/>
|
||||
<a :xlink:href="deployment.commitUrl">
|
||||
<text
|
||||
class="deploy-info-text deploy-info-text-link"
|
||||
transform="translate(20, 2)">
|
||||
{{ refText(deployment) }}
|
||||
</text>
|
||||
</a>
|
||||
</g>
|
||||
<!--Tag information-->
|
||||
<g
|
||||
transform="translate(5, 55)"
|
||||
v-if="deployment.tag">
|
||||
<icon
|
||||
name="label"
|
||||
:width="12"
|
||||
:height="12"
|
||||
:y="5"
|
||||
/>
|
||||
<a :xlink:href="deployment.tagUrl">
|
||||
<text
|
||||
class="deploy-info-text deploy-info-text-link"
|
||||
transform="translate(20, 2)"
|
||||
y="2"
|
||||
>
|
||||
{{ deployment.tag }}
|
||||
</text>
|
||||
</a>
|
||||
</g>
|
||||
</svg>
|
||||
stroke="#000">
|
||||
</line>
|
||||
</g>
|
||||
<svg
|
||||
height="0"
|
||||
width="0"
|
||||
>
|
||||
width="0">
|
||||
<defs>
|
||||
<linearGradient id="shadow-gradient">
|
||||
<linearGradient
|
||||
id="shadow-gradient">
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#000"
|
||||
stop-opacity="0.4"
|
||||
/>
|
||||
stop-opacity="0.4">
|
||||
</stop>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#000"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
stop-opacity="0">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script>
|
||||
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
|
||||
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -7,14 +9,15 @@
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentFlagPosition: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
deploymentFlagData: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
@ -23,74 +26,173 @@
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
realPixelRatio: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showFlagContent: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
timeSeries: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
unitOfDisplay: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
currentDataIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
legendTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
circleColorRgb: '#8fbce8',
|
||||
};
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
|
||||
computed: {
|
||||
formatTime() {
|
||||
return timeFormat(this.currentData.time);
|
||||
return this.deploymentFlagData ?
|
||||
timeFormat(this.deploymentFlagData.time) :
|
||||
timeFormat(this.currentData.time);
|
||||
},
|
||||
|
||||
formatDate() {
|
||||
return dateFormat(this.currentData.time);
|
||||
return this.deploymentFlagData ?
|
||||
dateFormat(this.deploymentFlagData.time) :
|
||||
dateFormat(this.currentData.time);
|
||||
},
|
||||
|
||||
calculatedHeight() {
|
||||
return this.graphHeight - this.graphHeightOffset;
|
||||
cursorStyle() {
|
||||
const xCoordinate = this.deploymentFlagData ?
|
||||
this.deploymentFlagData.xPos :
|
||||
this.currentXCoordinate;
|
||||
|
||||
const offsetTop = 20 * this.realPixelRatio;
|
||||
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
|
||||
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
|
||||
|
||||
return {
|
||||
top: `${offsetTop}px`,
|
||||
left: `${offsetLeft}px`,
|
||||
height: `${height}px`,
|
||||
};
|
||||
},
|
||||
|
||||
flagOrientation() {
|
||||
if (this.currentXCoordinate * this.realPixelRatio > 120) {
|
||||
return 'left';
|
||||
}
|
||||
return 'right';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
seriesMetricValue(series) {
|
||||
const index = this.deploymentFlagData ?
|
||||
this.deploymentFlagData.seriesIndex :
|
||||
this.currentDataIndex;
|
||||
const value = series.values[index] &&
|
||||
series.values[index].value;
|
||||
if (isNaN(value)) {
|
||||
return '-';
|
||||
}
|
||||
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
|
||||
},
|
||||
|
||||
seriesMetricLabel(index, series) {
|
||||
if (this.timeSeries.length < 2) {
|
||||
return this.legendTitle;
|
||||
}
|
||||
if (series.metricTag) {
|
||||
return series.metricTag;
|
||||
}
|
||||
return `series ${index + 1}`;
|
||||
},
|
||||
|
||||
strokeDashArray(type) {
|
||||
if (type === 'dashed') return '6, 3';
|
||||
if (type === 'dotted') return '3, 3';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<g class="mouse-over-flag">
|
||||
<line
|
||||
class="selected-metric-line"
|
||||
:x1="currentXCoordinate"
|
||||
:y1="0"
|
||||
:x2="currentXCoordinate"
|
||||
:y2="calculatedHeight"
|
||||
transform="translate(-5, 20)"
|
||||
/>
|
||||
<svg
|
||||
<div
|
||||
class="prometheus-graph-cursor"
|
||||
:style="cursorStyle"
|
||||
>
|
||||
<div
|
||||
v-if="showFlagContent"
|
||||
class="rect-text-metric"
|
||||
:x="currentFlagPosition"
|
||||
y="0"
|
||||
class="prometheus-graph-flag popover"
|
||||
:class="flagOrientation"
|
||||
>
|
||||
<rect
|
||||
class="rect-metric"
|
||||
x="4"
|
||||
y="1"
|
||||
rx="2"
|
||||
width="90"
|
||||
height="40"
|
||||
transform="translate(-3, 20)"
|
||||
/>
|
||||
<text
|
||||
class="text-metric text-metric-bold"
|
||||
x="16"
|
||||
y="35"
|
||||
transform="translate(-5, 20)"
|
||||
<div class="arrow"></div>
|
||||
<div class="popover-title">
|
||||
<h5 v-if="this.deploymentFlagData">
|
||||
Deployed
|
||||
</h5>
|
||||
{{formatDate}} at
|
||||
<strong>{{formatTime}}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="this.deploymentFlagData"
|
||||
class="popover-content deploy-meta-content"
|
||||
>
|
||||
{{ formatTime }}
|
||||
</text>
|
||||
<text
|
||||
class="text-metric"
|
||||
x="16"
|
||||
y="15"
|
||||
transform="translate(-5, 20)"
|
||||
<div>
|
||||
<icon
|
||||
name="commit"
|
||||
:size="12">
|
||||
</icon>
|
||||
<a :href="deploymentFlagData.commitUrl">
|
||||
{{deploymentFlagData.sha.slice(0, 8)}}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="deploymentFlagData.tag">
|
||||
<icon
|
||||
name="label"
|
||||
:size="12">
|
||||
</icon>
|
||||
<a :href="deploymentFlagData.tagUrl">
|
||||
{{deploymentFlagData.ref}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popover-content">
|
||||
<table>
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
>
|
||||
{{ formatDate }}
|
||||
</text>
|
||||
<td>
|
||||
<svg width="15" height="6">
|
||||
<line
|
||||
:stroke="series.lineColor"
|
||||
:stroke-dasharray="strokeDashArray(series.lineStyle)"
|
||||
stroke-width="4"
|
||||
x1="0"
|
||||
x2="15"
|
||||
y1="2"
|
||||
y2="2">
|
||||
</line>
|
||||
</svg>
|
||||
</g>
|
||||
</td>
|
||||
<td>{{seriesMetricLabel(index, series)}}</td>
|
||||
<td>
|
||||
<strong>{{seriesMetricValue(series)}}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -29,15 +29,18 @@ const mixins = {
|
|||
time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
|
||||
|
||||
if (xPos >= 0) {
|
||||
const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
|
||||
|
||||
deploymentDataArray.push({
|
||||
id: deployment.id,
|
||||
time,
|
||||
sha: deployment.sha,
|
||||
commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
|
||||
tag: deployment.tag,
|
||||
tagUrl: `${this.tagsPath}/${deployment.tag}`,
|
||||
tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
|
||||
ref: deployment.ref.name,
|
||||
xPos,
|
||||
seriesIndex,
|
||||
showDeploymentFlag: false,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const d3 = {
|
|||
timeYear,
|
||||
};
|
||||
|
||||
export const dateFormat = d3.time('%b %-d, %Y');
|
||||
export const dateFormat = d3.time('%a, %b %-d');
|
||||
export const timeFormat = d3.time('%-I:%M%p');
|
||||
export const dateFormatWithName = d3.time('%a, %b %-d');
|
||||
export const bisectDate = d3.bisector(d => d.time).left;
|
||||
|
|
|
@ -17,6 +17,11 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
|
||||
resetCachePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
ciLintPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -45,6 +50,14 @@ export default {
|
|||
Get started with Pipelines
|
||||
</a>
|
||||
|
||||
<a
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
:href="resetCachePath"
|
||||
class="btn btn-default">
|
||||
Clear runner caches
|
||||
</a>
|
||||
|
||||
<a
|
||||
:href="ciLintPath"
|
||||
class="btn btn-default">
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
canCreatePipeline: pipelinesData.canCreatePipeline,
|
||||
hasCi: pipelinesData.hasCi,
|
||||
ciLintPath: pipelinesData.ciLintPath,
|
||||
resetCachePath: pipelinesData.resetCachePath,
|
||||
state: this.store.state,
|
||||
scope: getParameterByName('scope') || 'all',
|
||||
page: getParameterByName('page') || '1',
|
||||
|
@ -220,6 +221,7 @@
|
|||
:new-pipeline-path="newPipelinePath"
|
||||
:has-ci-enabled="hasCiEnabled"
|
||||
:help-page-path="helpPagePath"
|
||||
:resetCachePath="resetCachePath"
|
||||
:ci-lint-path="ciLintPath"
|
||||
:can-create-pipeline="canCreatePipelineParsed "
|
||||
/>
|
||||
|
|
|
@ -541,7 +541,6 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
options.projectId = $(select).data('project-id');
|
||||
options.groupId = $(select).data('group-id');
|
||||
options.showCurrentUser = $(select).data('current-user');
|
||||
options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
|
||||
options.authorId = $(select).data('author-id');
|
||||
options.skipUsers = $(select).data('skip-users');
|
||||
showNullUser = $(select).data('null-user');
|
||||
|
@ -688,7 +687,6 @@ UsersSelect.prototype.users = function(query, options, callback) {
|
|||
todo_filter: options.todoFilter || null,
|
||||
todo_state_filter: options.todoStateFilter || null,
|
||||
current_user: options.showCurrentUser || null,
|
||||
push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
|
||||
author_id: options.authorId || null,
|
||||
skip_users: options.skipUsers || null
|
||||
},
|
||||
|
|
|
@ -13,16 +13,6 @@
|
|||
* - pipeline show page
|
||||
*/
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
ciIconBadge,
|
||||
loadingIcon,
|
||||
timeagoTooltip,
|
||||
userAvatarImage,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
|
@ -55,6 +45,22 @@
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
shouldRenderTriggeredLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
ciIconBadge,
|
||||
loadingIcon,
|
||||
timeagoTooltip,
|
||||
userAvatarImage,
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -81,7 +87,12 @@
|
|||
{{itemName}} #{{itemId}}
|
||||
</strong>
|
||||
|
||||
<template v-if="shouldRenderTriggeredLabel">
|
||||
triggered
|
||||
</template>
|
||||
<template v-else>
|
||||
created
|
||||
</template>
|
||||
|
||||
<timeago-tooltip :time="time" />
|
||||
|
||||
|
@ -92,8 +103,7 @@
|
|||
v-tooltip
|
||||
:href="user.path"
|
||||
:title="user.email"
|
||||
class="js-user-link commit-committer-link"
|
||||
>
|
||||
class="js-user-link commit-committer-link">
|
||||
|
||||
<user-avatar-image
|
||||
:img-src="user.avatar_url"
|
||||
|
@ -109,17 +119,13 @@
|
|||
|
||||
<section
|
||||
class="header-action-buttons"
|
||||
v-if="actions.length"
|
||||
>
|
||||
v-if="actions.length">
|
||||
<template
|
||||
v-for="(action, i) in actions"
|
||||
>
|
||||
v-for="action in actions">
|
||||
<a
|
||||
v-if="action.type === 'link'"
|
||||
:href="action.path"
|
||||
:class="action.cssClass"
|
||||
:key="i"
|
||||
>
|
||||
:class="action.cssClass">
|
||||
{{action.label}}
|
||||
</a>
|
||||
|
||||
|
@ -128,9 +134,7 @@
|
|||
:href="action.path"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
:class="action.cssClass"
|
||||
:key="i"
|
||||
>
|
||||
:class="action.cssClass">
|
||||
{{action.label}}
|
||||
</a>
|
||||
|
||||
|
@ -139,15 +143,12 @@
|
|||
@click="onClickAction(action)"
|
||||
:disabled="action.isLoading"
|
||||
:class="action.cssClass"
|
||||
type="button"
|
||||
:key="i"
|
||||
>
|
||||
type="button">
|
||||
{{action.label}}
|
||||
<i
|
||||
v-show="action.isLoading"
|
||||
class="fa fa-spin fa-spinner"
|
||||
aria-hidden="true"
|
||||
>
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</button>
|
||||
</template>
|
||||
|
@ -156,13 +157,11 @@
|
|||
type="button"
|
||||
class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
|
||||
aria-label="Toggle Sidebar"
|
||||
id="toggleSidebar"
|
||||
>
|
||||
id="toggleSidebar">
|
||||
<i
|
||||
class="fa fa-angle-double-left"
|
||||
aria-hidden="true"
|
||||
aria-labelledby="toggleSidebar"
|
||||
>
|
||||
aria-labelledby="toggleSidebar">
|
||||
</i>
|
||||
</button>
|
||||
</section>
|
||||
|
|
|
@ -20,10 +20,13 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
&.svg-250 {
|
||||
$image-widths: 250 306 394;
|
||||
@each $width in $image-widths {
|
||||
&.svg-#{$width} {
|
||||
img,
|
||||
svg {
|
||||
width: 250px;
|
||||
width: #{$width + 'px'};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,6 +192,17 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.name,
|
||||
small.aliases,
|
||||
small.params {
|
||||
float: left;
|
||||
}
|
||||
|
||||
small.aliases,
|
||||
small.params {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
small.description {
|
||||
float: right;
|
||||
padding: 3px 5px;
|
||||
|
@ -209,6 +220,7 @@
|
|||
}
|
||||
|
||||
ul > li {
|
||||
@include clearfix;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
@ -248,6 +248,73 @@
|
|||
}
|
||||
}
|
||||
|
||||
.prometheus-graph-cursor {
|
||||
position: absolute;
|
||||
background: $theme-gray-600;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.prometheus-graph-flag {
|
||||
display: block;
|
||||
min-width: 160px;
|
||||
|
||||
h5 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
|
||||
+ td {
|
||||
padding-left: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-meta-content {
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
svg {
|
||||
height: 15px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
&.popover {
|
||||
&.left {
|
||||
left: auto;
|
||||
right: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.right {
|
||||
left: 0;
|
||||
right: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> .arrow {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
> .popover-title,
|
||||
> .popover-content {
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prometheus-svg-container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
|
|
|
@ -92,6 +92,19 @@
|
|||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.multi-file-loading-container {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
|
||||
.animation-container {
|
||||
background: $gray-light;
|
||||
|
||||
div {
|
||||
background: $gray-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.table tr td.multi-file-table-name {
|
||||
width: 350px;
|
||||
padding: 6px 12px;
|
||||
|
|
|
@ -5,8 +5,6 @@ module IssuesAction
|
|||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def issues
|
||||
@finder_type = IssuesFinder
|
||||
@label = finder.labels.first
|
||||
|
||||
@issues = issuables_collection
|
||||
.non_archived
|
||||
.page(params[:page])
|
||||
|
|
|
@ -5,7 +5,6 @@ module MergeRequestsAction
|
|||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def merge_requests
|
||||
@finder_type = MergeRequestsFinder
|
||||
@label = finder.labels.first
|
||||
|
||||
@merge_requests = issuables_collection.page(params[:page])
|
||||
|
||||
|
|
|
@ -86,4 +86,8 @@ class Projects::ApplicationController < ApplicationController
|
|||
def require_pages_enabled!
|
||||
not_found unless @project.pages_available?
|
||||
end
|
||||
|
||||
def check_issues_available!
|
||||
return render_404 unless @project.feature_available?(:issues, current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ class Projects::BoardsController < Projects::ApplicationController
|
|||
include BoardsResponses
|
||||
include IssuableCollections
|
||||
|
||||
before_action :check_issues_available!
|
||||
before_action :authorize_read_board!, only: [:index, :show]
|
||||
before_action :assign_endpoint_vars
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class Projects::Clusters::GcpController < Projects::ApplicationController
|
||||
before_action :authorize_read_cluster!
|
||||
before_action :authorize_google_api, except: [:login]
|
||||
before_action :authorize_google_project_billing, only: [:new]
|
||||
before_action :authorize_create_cluster!, only: [:new, :create]
|
||||
|
||||
def login
|
||||
|
@ -22,15 +23,20 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
case google_project_billing_status
|
||||
when 'true'
|
||||
@cluster = ::Clusters::CreateService
|
||||
.new(project, current_user, create_params)
|
||||
.execute(token_in_session)
|
||||
|
||||
if @cluster.persisted?
|
||||
redirect_to project_cluster_path(project, @cluster)
|
||||
return redirect_to project_cluster_path(project, @cluster) if @cluster.persisted?
|
||||
when 'false'
|
||||
flash[:error] = _('Please enable billing for one of your projects to be able to create a cluster.')
|
||||
else
|
||||
render :new
|
||||
flash[:error] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
|
||||
end
|
||||
|
||||
render :new
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -58,6 +64,17 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def authorize_google_project_billing
|
||||
redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session)
|
||||
CheckGcpProjectBillingWorker.perform_async(redis_token_key)
|
||||
end
|
||||
|
||||
def google_project_billing_status
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.get(CheckGcpProjectBillingWorker.redis_shared_state_key_for(token_in_session))
|
||||
end
|
||||
end
|
||||
|
||||
def token_in_session
|
||||
@token_in_session ||=
|
||||
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
|
||||
|
|
|
@ -194,10 +194,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
|
||||
end
|
||||
|
||||
def check_issues_available!
|
||||
return render_404 unless @project.feature_available?(:issues, current_user)
|
||||
end
|
||||
|
||||
def render_issue_json
|
||||
if @issue.valid?
|
||||
render json: serializer.represent(@issue)
|
||||
|
|
|
@ -11,6 +11,16 @@ module Projects
|
|||
define_auto_devops_variables
|
||||
end
|
||||
|
||||
def reset_cache
|
||||
if ResetProjectCacheService.new(@project, current_user).execute
|
||||
flash[:notice] = _("Project cache successfully reset.")
|
||||
else
|
||||
flash[:error] = _("Unable to reset project cache.")
|
||||
end
|
||||
|
||||
redirect_to project_pipelines_path(@project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def define_runners_variables
|
||||
|
|
|
@ -374,18 +374,13 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def by_label(items)
|
||||
if labels?
|
||||
if filter_by_no_label?
|
||||
items = items.without_label
|
||||
else
|
||||
items = items.with_label(label_names, params[:sort])
|
||||
items_projects = projects(items)
|
||||
return items unless labels?
|
||||
|
||||
if items_projects
|
||||
label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
|
||||
items = items.where(labels: { id: label_ids })
|
||||
end
|
||||
end
|
||||
items =
|
||||
if filter_by_no_label?
|
||||
items.without_label
|
||||
else
|
||||
items.with_label(label_names, params[:sort])
|
||||
end
|
||||
|
||||
items
|
||||
|
|
|
@ -73,7 +73,6 @@ module SelectsHelper
|
|||
email_user: opts[:email_user] || false,
|
||||
first_user: opts[:first_user] && current_user ? current_user.username : false,
|
||||
current_user: opts[:current_user] || false,
|
||||
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
|
||||
author_id: opts[:author_id] || '',
|
||||
skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ module Ci
|
|||
before_save :ensure_token
|
||||
before_destroy { unscoped_project }
|
||||
|
||||
after_create do |build|
|
||||
after_create unless: :importing? do |build|
|
||||
run_after_commit { BuildHooksWorker.perform_async(build.id) }
|
||||
end
|
||||
|
||||
|
@ -461,7 +461,14 @@ module Ci
|
|||
end
|
||||
|
||||
def cache
|
||||
[options[:cache]]
|
||||
cache = options[:cache]
|
||||
|
||||
if cache && project.jobs_cache_index
|
||||
cache = cache.merge(
|
||||
key: "#{cache[:key]}:#{project.jobs_cache_index}")
|
||||
end
|
||||
|
||||
[cache]
|
||||
end
|
||||
|
||||
def credentials
|
||||
|
|
|
@ -371,7 +371,7 @@ class Commit
|
|||
#
|
||||
# Returns a symbol
|
||||
def uri_type(path)
|
||||
entry = @raw.tree.path(path)
|
||||
entry = @raw.rugged_tree_entry(path)
|
||||
if entry[:type] == :blob
|
||||
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
|
||||
blob.image? || blob.video? ? :raw : :blob
|
||||
|
|
|
@ -27,7 +27,7 @@ class PagesDomain < ActiveRecord::Base
|
|||
def url
|
||||
return unless domain
|
||||
|
||||
if certificate
|
||||
if certificate.present?
|
||||
"https://#{domain}"
|
||||
else
|
||||
"http://#{domain}"
|
||||
|
|
|
@ -1450,6 +1450,7 @@ class Project < ActiveRecord::Base
|
|||
import_finish
|
||||
remove_import_jid
|
||||
update_project_counter_caches
|
||||
after_create_default_branch
|
||||
end
|
||||
|
||||
def update_project_counter_caches
|
||||
|
@ -1463,6 +1464,27 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def after_create_default_branch
|
||||
return unless default_branch
|
||||
|
||||
# Ensure HEAD points to the default branch in case it is not master
|
||||
change_head(default_branch)
|
||||
|
||||
if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
|
||||
params = {
|
||||
name: default_branch,
|
||||
push_access_levels_attributes: [{
|
||||
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}],
|
||||
merge_access_levels_attributes: [{
|
||||
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}]
|
||||
}
|
||||
|
||||
ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_import_jid
|
||||
return unless import_jid
|
||||
|
||||
|
|
|
@ -783,34 +783,30 @@ class Repository
|
|||
end
|
||||
|
||||
def create_dir(user, path, **options)
|
||||
options[:user] = user
|
||||
options[:actions] = [{ action: :create_dir, file_path: path }]
|
||||
|
||||
multi_action(**options)
|
||||
multi_action(user, **options)
|
||||
end
|
||||
|
||||
def create_file(user, path, content, **options)
|
||||
options[:user] = user
|
||||
options[:actions] = [{ action: :create, file_path: path, content: content }]
|
||||
|
||||
multi_action(**options)
|
||||
multi_action(user, **options)
|
||||
end
|
||||
|
||||
def update_file(user, path, content, **options)
|
||||
previous_path = options.delete(:previous_path)
|
||||
action = previous_path && previous_path != path ? :move : :update
|
||||
|
||||
options[:user] = user
|
||||
options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
|
||||
|
||||
multi_action(**options)
|
||||
multi_action(user, **options)
|
||||
end
|
||||
|
||||
def delete_file(user, path, **options)
|
||||
options[:user] = user
|
||||
options[:actions] = [{ action: :delete, file_path: path }]
|
||||
|
||||
multi_action(**options)
|
||||
multi_action(user, **options)
|
||||
end
|
||||
|
||||
def with_cache_hooks
|
||||
|
@ -824,59 +820,14 @@ class Repository
|
|||
result.newrev
|
||||
end
|
||||
|
||||
def with_branch(user, *args)
|
||||
with_cache_hooks do
|
||||
Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
|
||||
yield start_commit
|
||||
end
|
||||
end
|
||||
def multi_action(user, **options)
|
||||
start_project = options.delete(:start_project)
|
||||
|
||||
if start_project
|
||||
options[:start_repository] = start_project.repository.raw_repository
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def multi_action(
|
||||
user:, branch_name:, message:, actions:,
|
||||
author_email: nil, author_name: nil,
|
||||
start_branch_name: nil, start_project: project)
|
||||
|
||||
with_branch(
|
||||
user,
|
||||
branch_name,
|
||||
start_branch_name: start_branch_name,
|
||||
start_repository: start_project.repository.raw_repository) do |start_commit|
|
||||
|
||||
index = Gitlab::Git::Index.new(raw_repository)
|
||||
|
||||
if start_commit
|
||||
index.read_tree(start_commit.rugged_commit.tree)
|
||||
parents = [start_commit.sha]
|
||||
else
|
||||
parents = []
|
||||
end
|
||||
|
||||
actions.each do |options|
|
||||
index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
options = {
|
||||
tree: index.write_tree,
|
||||
message: message,
|
||||
parents: parents
|
||||
}
|
||||
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
|
||||
|
||||
create_commit(options)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def get_committer_and_author(user, email: nil, name: nil)
|
||||
committer = user_to_committer(user)
|
||||
author = Gitlab::Git.committer_hash(email: email, name: name) || committer
|
||||
|
||||
{
|
||||
author: author,
|
||||
committer: committer
|
||||
}
|
||||
with_cache_hooks { raw.multi_action(user, **options) }
|
||||
end
|
||||
|
||||
def can_be_merged?(source_sha, target_branch)
|
||||
|
|
|
@ -241,7 +241,6 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { repository_disabled }.policy do
|
||||
prevent :push_code
|
||||
prevent :push_code_to_protected_branches
|
||||
prevent :download_code
|
||||
prevent :fork_project
|
||||
prevent :read_commit_status
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class GroupChildEntity < Grape::Entity
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include RequestAwareEntity
|
||||
include MarkupHelper
|
||||
|
||||
expose :id, :name, :description, :visibility, :full_name,
|
||||
:created_at, :updated_at, :avatar_url
|
||||
|
@ -59,6 +60,10 @@ class GroupChildEntity < Grape::Entity
|
|||
number_with_delimiter(instance.member_count)
|
||||
end
|
||||
|
||||
expose :markdown_description do |instance|
|
||||
markdown_description
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def membership
|
||||
|
@ -74,4 +79,8 @@ class GroupChildEntity < Grape::Entity
|
|||
def type
|
||||
object.class.name.downcase
|
||||
end
|
||||
|
||||
def markdown_description
|
||||
markdown_field(object, :description)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ class JobEntity < Grape::Entity
|
|||
expose :id
|
||||
expose :name
|
||||
|
||||
expose :started?, as: :started
|
||||
|
||||
expose :build_path do |build|
|
||||
build.target_url || path_to(:namespace_project_job, build)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
class CheckGcpProjectBillingService
|
||||
def execute(token)
|
||||
client = GoogleApi::CloudPlatform::Client.new(token, nil)
|
||||
client.projects_list.select do |project|
|
||||
client.projects_get_billing_info(project.name).billingEnabled
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ module Files
|
|||
|
||||
def create_commit!
|
||||
repository.multi_action(
|
||||
user: current_user,
|
||||
current_user,
|
||||
message: @commit_message,
|
||||
branch_name: @branch_name,
|
||||
actions: params[:actions],
|
||||
|
@ -13,6 +13,8 @@ module Files
|
|||
start_project: @start_project,
|
||||
start_branch_name: @start_branch
|
||||
)
|
||||
rescue ArgumentError => e
|
||||
raise_error(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -20,16 +22,7 @@ module Files
|
|||
def validate!
|
||||
super
|
||||
|
||||
params[:actions].each do |action|
|
||||
validate_action!(action)
|
||||
validate_file_status!(action)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_action!(action)
|
||||
unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s)
|
||||
raise_error("Unknown action '#{action[:action]}'")
|
||||
end
|
||||
params[:actions].each { |action| validate_file_status!(action) }
|
||||
end
|
||||
|
||||
def validate_file_status!(action)
|
||||
|
|
|
@ -154,24 +154,7 @@ class GitPushService < BaseService
|
|||
offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max
|
||||
@push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
|
||||
|
||||
# Ensure HEAD points to the default branch in case it is not master
|
||||
project.change_head(branch_name)
|
||||
|
||||
# Set protection on the default branch if configured
|
||||
if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch)
|
||||
|
||||
params = {
|
||||
name: @project.default_branch,
|
||||
push_access_levels_attributes: [{
|
||||
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}],
|
||||
merge_access_levels_attributes: [{
|
||||
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}]
|
||||
}
|
||||
|
||||
ProtectedBranches::CreateService.new(@project, current_user, params).execute
|
||||
end
|
||||
@project.after_create_default_branch
|
||||
end
|
||||
|
||||
def build_push_data
|
||||
|
|
|
@ -2,8 +2,8 @@ module ProtectedBranches
|
|||
class CreateService < BaseService
|
||||
attr_reader :protected_branch
|
||||
|
||||
def execute
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
||||
def execute(skip_authorization: false)
|
||||
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
|
||||
|
||||
project.protected_branches.create(params)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class ResetProjectCacheService < BaseService
|
||||
def execute
|
||||
@project.increment!(:jobs_cache_index)
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
.ide-flash-container.flash-container
|
||||
|
||||
#ide.ide-loading
|
||||
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
|
||||
.text-center
|
||||
= icon('spinner spin 2x')
|
||||
%h2.clgray= _('IDE Loading ...')
|
||||
%h2.clgray= _('Loading the GitLab IDE...')
|
||||
|
|
|
@ -49,6 +49,12 @@
|
|||
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
|
||||
Projects
|
||||
|
||||
- if current_controller?('ide')
|
||||
%li.line-separator.hidden-xs
|
||||
= nav_link(controller: 'ide') do
|
||||
= link_to '#', class: 'dashboard-shortcuts-web-ide', title: 'Web IDE' do
|
||||
Web IDE
|
||||
|
||||
- if current_user.admin? || Gitlab::Sherlock.enabled?
|
||||
%li.line-separator.hidden-xs
|
||||
- if current_user.admin?
|
||||
|
|
|
@ -299,6 +299,7 @@
|
|||
Charts
|
||||
|
||||
-# Shortcut to Issues > New Issue
|
||||
- if project_nav_tab?(:issues)
|
||||
%li.hidden
|
||||
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
|
||||
Create a new issue
|
||||
|
@ -316,5 +317,6 @@
|
|||
Commits
|
||||
|
||||
-# Shortcut to issue boards
|
||||
- if project_nav_tab?(:issues)
|
||||
%li.hidden
|
||||
= link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
|
||||
|
|
|
@ -10,5 +10,5 @@
|
|||
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
|
||||
%li
|
||||
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), target: '_blank', rel: 'noopener noreferrer')
|
||||
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
- if @authorize_url
|
||||
= link_to @authorize_url do
|
||||
= image_tag('auth_buttons/signin_with_google.png', width: '191px')
|
||||
= _('or')
|
||||
= link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
|
||||
- else
|
||||
- link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
- illustration = local_assigns.fetch(:illustration)
|
||||
- illustration_size = local_assigns.fetch(:illustration_size)
|
||||
- title = local_assigns.fetch(:title)
|
||||
- content = local_assigns.fetch(:content)
|
||||
- action = local_assigns.fetch(:action, nil)
|
||||
|
||||
.row.empty-state
|
||||
.col-xs-12
|
||||
.svg-content{ class: illustration_size }
|
||||
= image_tag illustration
|
||||
.col-xs-12
|
||||
.text-content
|
||||
%h4.text-center= title
|
||||
%p= content
|
||||
- if action
|
||||
.text-center
|
||||
= action
|
|
@ -54,7 +54,7 @@
|
|||
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
|
||||
- else
|
||||
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
|
||||
|
||||
- if @build.started?
|
||||
.build-trace-container.prepend-top-default
|
||||
.top-bar.js-top-bar
|
||||
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
|
||||
|
@ -88,7 +88,19 @@
|
|||
%pre.build-trace#build-trace
|
||||
%code.bash.js-build-output
|
||||
.build-loader-animation.js-build-refresh
|
||||
|
||||
- elsif @build.playable?
|
||||
= render 'empty_state',
|
||||
illustration: 'illustrations/manual_action.svg',
|
||||
illustration_size: 'svg-394',
|
||||
title: _('This job requires a manual action'),
|
||||
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.'),
|
||||
action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), class: 'btn btn-primary', title: _('Trigger this manual action') )
|
||||
- else
|
||||
= render 'empty_state',
|
||||
illustration: 'illustrations/job_not_triggered.svg',
|
||||
illustration_size: 'svg-306',
|
||||
title: _('This job has not been triggered yet'),
|
||||
content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered.')
|
||||
|
||||
= render "sidebar"
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"new-pipeline-path" => new_project_pipeline_path(@project),
|
||||
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
|
||||
"has-ci" => @repository.gitlab_ci_yml,
|
||||
"ci-lint-path" => ci_lint_path } }
|
||||
"ci-lint-path" => ci_lint_path,
|
||||
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
|
||||
|
||||
= page_specific_javascript_bundle_tag('common_vue')
|
||||
= page_specific_javascript_bundle_tag('pipelines')
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
- gcp_cluster:cluster_provision
|
||||
- gcp_cluster:cluster_wait_for_app_installation
|
||||
- gcp_cluster:wait_for_cluster_creation
|
||||
- gcp_cluster:check_gcp_project_billing
|
||||
|
||||
- github_import_advance_stage
|
||||
- github_importer:github_import_import_diff_note
|
||||
|
|
|
@ -1,10 +1,53 @@
|
|||
class BackgroundMigrationWorker
|
||||
include ApplicationWorker
|
||||
|
||||
# The minimum amount of time between processing two jobs of the same migration
|
||||
# class.
|
||||
#
|
||||
# This interval is set to 5 minutes so autovacuuming and other maintenance
|
||||
# related tasks have plenty of time to clean up after a migration has been
|
||||
# performed.
|
||||
MIN_INTERVAL = 5.minutes.to_i
|
||||
|
||||
# Performs the background migration.
|
||||
#
|
||||
# See Gitlab::BackgroundMigration.perform for more information.
|
||||
#
|
||||
# class_name - The class name of the background migration to run.
|
||||
# arguments - The arguments to pass to the migration class.
|
||||
def perform(class_name, arguments = [])
|
||||
should_perform, ttl = perform_and_ttl(class_name)
|
||||
|
||||
if should_perform
|
||||
Gitlab::BackgroundMigration.perform(class_name, arguments)
|
||||
else
|
||||
# If the lease could not be obtained this means either another process is
|
||||
# running a migration of this class or we ran one recently. In this case
|
||||
# we'll reschedule the job in such a way that it is picked up again around
|
||||
# the time the lease expires.
|
||||
self.class.perform_in(ttl || MIN_INTERVAL, class_name, arguments)
|
||||
end
|
||||
end
|
||||
|
||||
def perform_and_ttl(class_name)
|
||||
if always_perform?
|
||||
# In test environments `perform_in` will run right away. This can then
|
||||
# lead to stack level errors in the above `#perform`. To work around this
|
||||
# we'll just perform the migration right away in the test environment.
|
||||
[true, nil]
|
||||
else
|
||||
lease = lease_for(class_name)
|
||||
|
||||
[lease.try_obtain, lease.ttl]
|
||||
end
|
||||
end
|
||||
|
||||
def lease_for(class_name)
|
||||
Gitlab::ExclusiveLease
|
||||
.new("#{self.class.name}:#{class_name}", timeout: MIN_INTERVAL)
|
||||
end
|
||||
|
||||
def always_perform?
|
||||
Rails.env.test?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
require 'securerandom'
|
||||
|
||||
class CheckGcpProjectBillingWorker
|
||||
include ApplicationWorker
|
||||
include ClusterQueue
|
||||
|
||||
LEASE_TIMEOUT = 15.seconds.to_i
|
||||
SESSION_KEY_TIMEOUT = 5.minutes
|
||||
BILLING_TIMEOUT = 1.hour
|
||||
|
||||
def self.get_session_token(token_key)
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.get(get_redis_session_key(token_key))
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_session_token(token)
|
||||
generate_token_key.tap do |token_key|
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.redis_shared_state_key_for(token)
|
||||
"gitlab:gcp:#{token.hash}:billing_enabled"
|
||||
end
|
||||
|
||||
def perform(token_key)
|
||||
return unless token_key
|
||||
|
||||
token = self.get_session_token(token_key)
|
||||
return unless token
|
||||
return unless try_obtain_lease_for(token)
|
||||
|
||||
billing_enabled_projects = CheckGcpProjectBillingService.new.execute(token)
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.set(self.class.redis_shared_state_key_for(token),
|
||||
!billing_enabled_projects.empty?,
|
||||
ex: BILLING_TIMEOUT)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.generate_token_key
|
||||
SecureRandom.uuid
|
||||
end
|
||||
|
||||
def self.get_redis_session_key(token_key)
|
||||
"gitlab:gcp:session:#{token_key}"
|
||||
end
|
||||
|
||||
def try_obtain_lease_for(token)
|
||||
Gitlab::ExclusiveLease
|
||||
.new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
|
||||
.try_obtain
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Generate HTTP URLs for custom Pages domains when appropriate
|
||||
merge_request: 16279
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display graph values on hover within monitoring page
|
||||
merge_request: 16261
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Protected branch is now created for default branch on import
|
||||
merge_request: 16198
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Implement checking GCP project billing status in cluster creation form.
|
||||
merge_request: 15665
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "Fix slash commands dropdown description mis-alignment on Firefox"
|
||||
merge_request: 16125
|
||||
author: Maurizio De Santis
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate existing data from KubernetesService to Clusters::Platforms::Kubernetes
|
||||
merge_request: 15589
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rendering of emoji's in Group-Overview
|
||||
merge_request: 16098
|
||||
author: Jacopo Beschi @jacopo-beschi
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: disables shortcut to issue boards when issues are not enabled
|
||||
merge_request: 16020
|
||||
author: Christiaan Van den Poel
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Implement project jobs cache reset
|
||||
merge_request: 16067
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update scss-lint to 0.56.0
|
||||
merge_request: 16278
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Run background migrations with a minimum interval
|
||||
merge_request:
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Fix missing references to pipeline objects when restoring project with import/export
|
||||
feature
|
||||
merge_request: 16221
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix timeout when filtering issues by label
|
||||
merge_request:
|
||||
author:
|
||||
type: performance
|
|
@ -408,7 +408,9 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
end
|
||||
namespace :settings do
|
||||
get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
|
||||
resource :ci_cd, only: [:show], controller: 'ci_cd'
|
||||
resource :ci_cd, only: [:show], controller: 'ci_cd' do
|
||||
post :reset_cache
|
||||
end
|
||||
resource :integrations, only: [:show]
|
||||
resource :repository, only: [:show], controller: :repository
|
||||
end
|
||||
|
|
|
@ -63,7 +63,8 @@ Sidekiq::Testing.inline! do
|
|||
namespace_id: group.id,
|
||||
name: project_path.titleize,
|
||||
description: FFaker::Lorem.sentence,
|
||||
visibility_level: Gitlab::VisibilityLevel.values.sample
|
||||
visibility_level: Gitlab::VisibilityLevel.values.sample,
|
||||
skip_disk_validation: true
|
||||
}
|
||||
|
||||
project = Projects::CreateService.new(User.first, params).execute
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# This migration is a duplicate of 20171230123729_add_rebase_commit_sha_to_merge_requests_ce.rb
|
||||
#
|
||||
# We backported this feature from EE using the same migration, but with a new
|
||||
# timestamp, which caused an error when the backport was then to be merged back
|
||||
# into EE.
|
||||
#
|
||||
# See discussion at https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3932
|
||||
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
unless column_exists?(:merge_requests, :rebase_commit_sha)
|
||||
add_column :merge_requests, :rebase_commit_sha, :string
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
if column_exists?(:merge_requests, :rebase_commit_sha)
|
||||
remove_column :merge_requests, :rebase_commit_sha
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddJobsCacheIndexToProject < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :projects, :jobs_cache_index, :integer
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :merge_requests, :rebase_commit_sha, :string
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
class AddRebaseCommitShaToMergeRequestsCe < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
unless column_exists?(:merge_requests, :rebase_commit_sha)
|
||||
add_column :merge_requests, :rebase_commit_sha, :string
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
if column_exists?(:merge_requests, :rebase_commit_sha)
|
||||
remove_column :merge_requests, :rebase_commit_sha
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,151 @@
|
|||
class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME = 'KubernetesService'.freeze
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class Project < ActiveRecord::Base
|
||||
self.table_name = 'projects'
|
||||
|
||||
has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
|
||||
has_many :clusters, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
|
||||
has_many :services, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service'
|
||||
has_one :kubernetes_service, -> { where(category: 'deployment', type: 'KubernetesService') }, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Service', inverse_of: :project, foreign_key: :project_id
|
||||
end
|
||||
|
||||
class Cluster < ActiveRecord::Base
|
||||
self.table_name = 'clusters'
|
||||
|
||||
has_many :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::ClustersProject'
|
||||
has_many :projects, through: :cluster_projects, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
|
||||
has_one :platform_kubernetes, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::PlatformsKubernetes'
|
||||
|
||||
accepts_nested_attributes_for :platform_kubernetes
|
||||
|
||||
enum platform_type: {
|
||||
kubernetes: 1
|
||||
}
|
||||
|
||||
enum provider_type: {
|
||||
user: 0,
|
||||
gcp: 1
|
||||
}
|
||||
end
|
||||
|
||||
class ClustersProject < ActiveRecord::Base
|
||||
self.table_name = 'cluster_projects'
|
||||
|
||||
belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
|
||||
belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project'
|
||||
end
|
||||
|
||||
class PlatformsKubernetes < ActiveRecord::Base
|
||||
self.table_name = 'cluster_platforms_kubernetes'
|
||||
|
||||
belongs_to :cluster, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Cluster'
|
||||
|
||||
attr_encrypted :token,
|
||||
mode: :per_attribute_iv,
|
||||
key: Gitlab::Application.secrets.db_key_base,
|
||||
algorithm: 'aes-256-cbc'
|
||||
end
|
||||
|
||||
class Service < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'services'
|
||||
self.inheritance_column = :_type_disabled # Disable STI, otherwise KubernetesModel will be looked up
|
||||
|
||||
belongs_to :project, class_name: 'MigrateKubernetesServiceToNewClustersArchitectures::Project', foreign_key: :project_id
|
||||
|
||||
scope :unmanaged_kubernetes_service, -> do
|
||||
joins('LEFT JOIN projects ON projects.id = services.project_id')
|
||||
.joins('LEFT JOIN cluster_projects ON cluster_projects.project_id = projects.id')
|
||||
.joins('LEFT JOIN cluster_platforms_kubernetes ON cluster_platforms_kubernetes.cluster_id = cluster_projects.cluster_id')
|
||||
.where(category: 'deployment', type: 'KubernetesService', template: false)
|
||||
.where("services.properties LIKE '%api_url%'")
|
||||
.where("(services.properties NOT LIKE CONCAT('%', cluster_platforms_kubernetes.api_url, '%')) OR cluster_platforms_kubernetes.api_url IS NULL")
|
||||
.group(:id)
|
||||
.order(id: :asc)
|
||||
end
|
||||
|
||||
scope :kubernetes_service_without_template, -> do
|
||||
where(category: 'deployment', type: 'KubernetesService', template: false)
|
||||
end
|
||||
|
||||
def api_url
|
||||
parsed_properties['api_url']
|
||||
end
|
||||
|
||||
def ca_pem
|
||||
parsed_properties['ca_pem']
|
||||
end
|
||||
|
||||
def namespace
|
||||
parsed_properties['namespace']
|
||||
end
|
||||
|
||||
def token
|
||||
parsed_properties['token']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parsed_properties
|
||||
@parsed_properties ||= JSON.parse(self.properties)
|
||||
end
|
||||
end
|
||||
|
||||
def find_dedicated_environement_scope(project)
|
||||
environment_scopes = project.clusters.map(&:environment_scope)
|
||||
|
||||
return '*' if environment_scopes.exclude?('*') # KubernetesService should be added as a default cluster (environment_scope: '*') at first place
|
||||
return 'migrated/*' if environment_scopes.exclude?('migrated/*') # If it's conflicted, the KubernetesService added as a migrated cluster
|
||||
|
||||
unique_iid = 0
|
||||
|
||||
# If it's still conflicted, finding an unique environment scope incrementaly
|
||||
loop do
|
||||
candidate = "migrated#{unique_iid}/*"
|
||||
return candidate if environment_scopes.exclude?(candidate)
|
||||
|
||||
unique_iid += 1
|
||||
end
|
||||
end
|
||||
|
||||
def up
|
||||
ActiveRecord::Base.transaction do
|
||||
MigrateKubernetesServiceToNewClustersArchitectures::Service
|
||||
.unmanaged_kubernetes_service.find_each(batch_size: 1) do |kubernetes_service|
|
||||
MigrateKubernetesServiceToNewClustersArchitectures::Cluster.create(
|
||||
enabled: kubernetes_service.active,
|
||||
user_id: nil, # KubernetesService doesn't have
|
||||
name: DEFAULT_KUBERNETES_SERVICE_CLUSTER_NAME,
|
||||
provider_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.provider_types[:user],
|
||||
platform_type: MigrateKubernetesServiceToNewClustersArchitectures::Cluster.platform_types[:kubernetes],
|
||||
projects: [kubernetes_service.project],
|
||||
environment_scope: find_dedicated_environement_scope(kubernetes_service.project),
|
||||
platform_kubernetes_attributes: {
|
||||
api_url: kubernetes_service.api_url,
|
||||
ca_cert: kubernetes_service.ca_pem,
|
||||
namespace: kubernetes_service.namespace,
|
||||
username: nil, # KubernetesService doesn't have
|
||||
encrypted_password: nil, # KubernetesService doesn't have
|
||||
encrypted_password_iv: nil, # KubernetesService doesn't have
|
||||
token: kubernetes_service.token # encrypted_token and encrypted_token_iv
|
||||
} )
|
||||
end
|
||||
end
|
||||
|
||||
MigrateKubernetesServiceToNewClustersArchitectures::Service
|
||||
.kubernetes_service_without_template.each_batch(of: 100) do |kubernetes_service|
|
||||
kubernetes_service.update_all(active: false)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# noop
|
||||
end
|
||||
end
|
|
@ -1451,6 +1451,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do
|
|||
t.boolean "repository_read_only"
|
||||
t.boolean "merge_requests_ff_only_enabled", default: false
|
||||
t.boolean "merge_requests_rebase_enabled", default: false, null: false
|
||||
t.integer "jobs_cache_index"
|
||||
end
|
||||
|
||||
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
|
||||
|
|
|
@ -15,7 +15,7 @@ request introducing these changes must be accompanied by the documentation
|
|||
(either updating existing ones or creating new ones). This is also valid when
|
||||
changes are introduced to the UI.
|
||||
|
||||
The one resposible for writing the first piece of documentation is the developer who
|
||||
The one responsible for writing the first piece of documentation is the developer who
|
||||
wrote the code. It's the job of the Product Manager to ensure all features are
|
||||
shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
|
||||
this is the only way to keep the docs up-to-date. If you have any questions about it,
|
||||
|
@ -31,7 +31,7 @@ Every major feature (regardless if present in GitLab Community or Enterprise edi
|
|||
should present, at the beginning of the document, two main sections: **overview** and
|
||||
**use cases**. Every GitLab EE-only feature should also contain these sections.
|
||||
|
||||
**Overview**: at the name suggests, the goal here is to provide an overview of the feature.
|
||||
**Overview**: as the name suggests, the goal here is to provide an overview of the feature.
|
||||
Describe what is it, what it does, why it is important/cool/nice-to-have,
|
||||
what problem it solves, and what you can do with this feature that you couldn't
|
||||
do before.
|
||||
|
|
|
@ -299,9 +299,9 @@ sudo usermod -aG redis git
|
|||
### Clone the Source
|
||||
|
||||
# Clone GitLab repository
|
||||
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-3-stable gitlab
|
||||
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-4-stable gitlab
|
||||
|
||||
**Note:** You can change `10-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
|
||||
**Note:** You can change `10-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
|
||||
|
||||
### Configure It
|
||||
|
||||
|
|
|
@ -30,7 +30,8 @@ with all their related data and be moved into a new GitLab instance.
|
|||
|
||||
| GitLab version | Import/Export version |
|
||||
| ---------------- | --------------------- |
|
||||
| 10.3 to current | 0.2.1 |
|
||||
| 10.4 to current | 0.2.2 |
|
||||
| 10.3 | 0.2.1 |
|
||||
| 10.0 | 0.2.0 |
|
||||
| 9.4.0 | 0.1.8 |
|
||||
| 9.2.0 | 0.1.7 |
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
@public
|
||||
Feature: Explore Groups
|
||||
Background:
|
||||
Given group "TestGroup" has private project "Enterprise"
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group with private and internal projects as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
When I sign in as a user
|
||||
And I visit group "TestGroup" page
|
||||
Then I should see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group issues for internal project as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
When I sign in as a user
|
||||
And I visit group "TestGroup" issues page
|
||||
Then I should see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group merge requests for internal project as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
When I sign in as a user
|
||||
And I visit group "TestGroup" merge requests page
|
||||
Then I should see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group with private, internal and public projects as visitor
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I visit group "TestGroup" page
|
||||
Then I should see project "Community" items
|
||||
And I should not see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group issues for public project as visitor
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I visit group "TestGroup" issues page
|
||||
Then I should see project "Community" items
|
||||
And I should not see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group merge requests for public project as visitor
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I visit group "TestGroup" merge requests page
|
||||
Then I should see project "Community" items
|
||||
And I should not see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group with private, internal and public projects as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I sign in as a user
|
||||
And I visit group "TestGroup" page
|
||||
Then I should see project "Community" items
|
||||
And I should see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group issues for internal and public projects as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I sign in as a user
|
||||
And I visit group "TestGroup" issues page
|
||||
Then I should see project "Community" items
|
||||
And I should see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group merge requests for internal and public projects as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I sign in as a user
|
||||
And I visit group "TestGroup" merge requests page
|
||||
Then I should see project "Community" items
|
||||
And I should see project "Internal" items
|
||||
And I should not see project "Enterprise" items
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group with public project in public groups area
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I visit the public groups area
|
||||
Then I should see group "TestGroup"
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group with public project in public groups area as user
|
||||
Given group "TestGroup" has public project "Community"
|
||||
When I sign in as a user
|
||||
And I visit the public groups area
|
||||
Then I should see group "TestGroup"
|
||||
|
||||
@javascript
|
||||
Scenario: I should see group with internal project in public groups area as user
|
||||
Given group "TestGroup" has internal project "Internal"
|
||||
When I sign in as a user
|
||||
And I visit the public groups area
|
||||
Then I should see group "TestGroup"
|
|
@ -1,45 +0,0 @@
|
|||
Feature: Invites
|
||||
Background:
|
||||
Given "John Doe" is owner of group "Owned"
|
||||
And "John Doe" has invited "user@example.com" to group "Owned"
|
||||
|
||||
Scenario: Viewing invitation when signed out
|
||||
When I visit the invitation page
|
||||
Then I should be redirected to the sign in page
|
||||
And I should see a notice telling me to sign in
|
||||
|
||||
Scenario: Signing in to view invitation
|
||||
When I visit the invitation page
|
||||
And I sign in as "Mary Jane"
|
||||
Then I should be redirected to the invitation page
|
||||
|
||||
Scenario: Viewing invitation when signed in
|
||||
Given I sign in as "Mary Jane"
|
||||
And I visit the invitation page
|
||||
Then I should see the invitation details
|
||||
And I should see an "Accept invitation" button
|
||||
And I should see a "Decline" button
|
||||
|
||||
Scenario: Viewing invitation as an existing member
|
||||
Given I sign in as "John Doe"
|
||||
And I visit the invitation page
|
||||
Then I should see a message telling me I'm already a member
|
||||
|
||||
Scenario: Accepting the invitation
|
||||
Given I sign in as "Mary Jane"
|
||||
And I visit the invitation page
|
||||
And I click the "Accept invitation" button
|
||||
Then I should be redirected to the group page
|
||||
And I should see a notice telling me I have access
|
||||
|
||||
Scenario: Declining the application when signed in
|
||||
Given I sign in as "Mary Jane"
|
||||
And I visit the invitation page
|
||||
And I click the "Decline" button
|
||||
Then I should be redirected to the dashboard
|
||||
And I should see a notice telling me I have declined
|
||||
|
||||
Scenario: Declining the application when signed out
|
||||
When I visit the invitation's decline page
|
||||
Then I should be redirected to the sign in page
|
||||
And I should see a notice telling me I have declined
|
|
@ -1,88 +0,0 @@
|
|||
class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
|
||||
include SharedAuthentication
|
||||
include SharedPaths
|
||||
include SharedGroup
|
||||
include SharedProject
|
||||
|
||||
step 'group "TestGroup" has private project "Enterprise"' do
|
||||
group_has_project("TestGroup", "Enterprise", Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
step 'group "TestGroup" has internal project "Internal"' do
|
||||
group_has_project("TestGroup", "Internal", Gitlab::VisibilityLevel::INTERNAL)
|
||||
end
|
||||
|
||||
step 'group "TestGroup" has public project "Community"' do
|
||||
group_has_project("TestGroup", "Community", Gitlab::VisibilityLevel::PUBLIC)
|
||||
end
|
||||
|
||||
step '"John Doe" is owner of group "TestGroup"' do
|
||||
group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup")
|
||||
user = create(:user, name: "John Doe")
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
step 'I visit group "TestGroup" page' do
|
||||
visit group_path(Group.find_by(name: "TestGroup"))
|
||||
end
|
||||
|
||||
step 'I visit group "TestGroup" issues page' do
|
||||
visit issues_group_path(Group.find_by(name: "TestGroup"))
|
||||
end
|
||||
|
||||
step 'I visit group "TestGroup" merge requests page' do
|
||||
visit merge_requests_group_path(Group.find_by(name: "TestGroup"))
|
||||
end
|
||||
|
||||
step 'I visit group "TestGroup" members page' do
|
||||
visit group_group_members_path(Group.find_by(name: "TestGroup"))
|
||||
end
|
||||
|
||||
step 'I should not see project "Enterprise" items' do
|
||||
expect(page).not_to have_content "Enterprise"
|
||||
end
|
||||
|
||||
step 'I should see project "Internal" items' do
|
||||
expect(page).to have_content "Internal"
|
||||
end
|
||||
|
||||
step 'I should not see project "Internal" items' do
|
||||
expect(page).not_to have_content "Internal"
|
||||
end
|
||||
|
||||
step 'I should see project "Community" items' do
|
||||
expect(page).to have_content "Community"
|
||||
end
|
||||
|
||||
step 'I change filter to Everyone\'s' do
|
||||
click_link "Everyone's"
|
||||
end
|
||||
|
||||
step 'I should see group member "John Doe"' do
|
||||
expect(page).to have_content "John Doe"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def group_has_project(groupname, projectname, visibility_level)
|
||||
group = Group.find_by(name: groupname) || create(:group, name: groupname)
|
||||
project = create(:project,
|
||||
namespace: group,
|
||||
name: projectname,
|
||||
path: "#{groupname}-#{projectname}",
|
||||
visibility_level: visibility_level
|
||||
)
|
||||
create(:issue,
|
||||
title: "#{projectname} feature",
|
||||
project: project
|
||||
)
|
||||
create(:merge_request,
|
||||
title: "#{projectname} feature implemented",
|
||||
source_project: project,
|
||||
target_project: project
|
||||
)
|
||||
create(:closed_issue_event,
|
||||
project: project
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,80 +0,0 @@
|
|||
class Spinach::Features::Invites < Spinach::FeatureSteps
|
||||
include SharedAuthentication
|
||||
include SharedUser
|
||||
include SharedGroup
|
||||
|
||||
step '"John Doe" has invited "user@example.com" to group "Owned"' do
|
||||
user = User.find_by(name: "John Doe")
|
||||
group = Group.find_by(name: "Owned")
|
||||
group.add_developer("user@example.com", user)
|
||||
end
|
||||
|
||||
step 'I visit the invitation page' do
|
||||
group = Group.find_by(name: "Owned")
|
||||
invite = group.group_members.invite.last
|
||||
invite.generate_invite_token!
|
||||
@raw_invite_token = invite.raw_invite_token
|
||||
visit invite_path(@raw_invite_token)
|
||||
end
|
||||
|
||||
step 'I should be redirected to the sign in page' do
|
||||
expect(current_path).to eq(new_user_session_path)
|
||||
end
|
||||
|
||||
step 'I should see a notice telling me to sign in' do
|
||||
expect(page).to have_content "To accept this invitation, sign in"
|
||||
end
|
||||
|
||||
step 'I should be redirected to the invitation page' do
|
||||
expect(current_path).to eq(invite_path(@raw_invite_token))
|
||||
end
|
||||
|
||||
step 'I should see the invitation details' do
|
||||
expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.")
|
||||
end
|
||||
|
||||
step "I should see a message telling me I'm already a member" do
|
||||
expect(page).to have_content("However, you are already a member of this group.")
|
||||
end
|
||||
|
||||
step 'I should see an "Accept invitation" button' do
|
||||
expect(page).to have_link("Accept invitation")
|
||||
end
|
||||
|
||||
step 'I should see a "Decline" button' do
|
||||
expect(page).to have_link("Decline")
|
||||
end
|
||||
|
||||
step 'I click the "Accept invitation" button' do
|
||||
page.click_link "Accept invitation"
|
||||
end
|
||||
|
||||
step 'I should be redirected to the group page' do
|
||||
group = Group.find_by(name: "Owned")
|
||||
expect(current_path).to eq(group_path(group))
|
||||
end
|
||||
|
||||
step 'I should see a notice telling me I have access' do
|
||||
expect(page).to have_content("You have been granted Developer access to group Owned.")
|
||||
end
|
||||
|
||||
step 'I click the "Decline" button' do
|
||||
page.click_link "Decline"
|
||||
end
|
||||
|
||||
step 'I should be redirected to the dashboard' do
|
||||
expect(current_path).to eq(dashboard_projects_path)
|
||||
end
|
||||
|
||||
step 'I should see a notice telling me I have declined' do
|
||||
expect(page).to have_content("You have declined the invitation to join group Owned.")
|
||||
end
|
||||
|
||||
step "I visit the invitation's decline page" do
|
||||
group = Group.find_by(name: "Owned")
|
||||
invite = group.group_members.invite.last
|
||||
invite.generate_invite_token!
|
||||
@raw_invite_token = invite.raw_invite_token
|
||||
visit decline_invite_path(@raw_invite_token)
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@ module SharedBuilds
|
|||
|
||||
step 'project has a recent build' do
|
||||
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
|
||||
@build = create(:ci_build, :coverage, pipeline: @pipeline)
|
||||
@build = create(:ci_build, :running, :coverage, pipeline: @pipeline)
|
||||
end
|
||||
|
||||
step 'recent build is successful' do
|
||||
|
|
|
@ -842,6 +842,12 @@ into similar problems in the future (e.g. when new tables are created).
|
|||
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
|
||||
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
|
||||
|
||||
# To not overload the worker too much we enforce a minimum interval both
|
||||
# when scheduling and performing jobs.
|
||||
if delay_interval < BackgroundMigrationWorker::MIN_INTERVAL
|
||||
delay_interval = BackgroundMigrationWorker::MIN_INTERVAL
|
||||
end
|
||||
|
||||
model_class.each_batch(of: batch_size) do |relation, index|
|
||||
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
|
||||
|
||||
|
|
|
@ -71,5 +71,16 @@ module Gitlab
|
|||
redis.exists(@redis_shared_state_key)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the TTL of the Redis key.
|
||||
#
|
||||
# This method will return `nil` if no TTL could be obtained.
|
||||
def ttl
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
ttl = redis.ttl(@redis_shared_state_key)
|
||||
|
||||
ttl if ttl.positive?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -173,8 +173,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def find_by_rugged(repository, sha, path, limit:)
|
||||
commit = repository.lookup(sha)
|
||||
root_tree = commit.tree
|
||||
rugged_commit = repository.lookup(sha)
|
||||
root_tree = rugged_commit.tree
|
||||
|
||||
blob_entry = find_entry_by_path(repository, root_tree.oid, path)
|
||||
|
||||
|
|
|
@ -15,8 +15,6 @@ module Gitlab
|
|||
|
||||
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
|
||||
|
||||
delegate :tree, to: :rugged_commit
|
||||
|
||||
def ==(other)
|
||||
return false unless other.is_a?(Gitlab::Git::Commit)
|
||||
|
||||
|
@ -452,6 +450,11 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
# Is this the same as Blob.find_entry_by_path ?
|
||||
def rugged_tree_entry(path)
|
||||
rugged_commit.tree.path(path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_from_hash(hash)
|
||||
|
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
DEFAULT_MODE = 0o100644
|
||||
|
||||
ACTIONS = %w(create create_dir update move delete).freeze
|
||||
ACTION_OPTIONS = %i(file_path previous_path content encoding).freeze
|
||||
|
||||
attr_reader :repository, :raw_index
|
||||
|
||||
|
@ -20,6 +21,11 @@ module Gitlab
|
|||
|
||||
delegate :read_tree, :get, to: :raw_index
|
||||
|
||||
def apply(action, options)
|
||||
validate_action!(action)
|
||||
public_send(action, options.slice(*ACTION_OPTIONS)) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
def write_tree
|
||||
raw_index.write_tree(repository.rugged)
|
||||
end
|
||||
|
@ -140,6 +146,12 @@ module Gitlab
|
|||
rescue Rugged::IndexError => e
|
||||
raise IndexError, e.message
|
||||
end
|
||||
|
||||
def validate_action!(action)
|
||||
unless ACTIONS.include?(action.to_s)
|
||||
raise ArgumentError, "Unknown action '#{action}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1163,23 +1163,13 @@ module Gitlab
|
|||
end
|
||||
|
||||
def fetch_repository_as_mirror(repository)
|
||||
remote_name = "tmp-#{SecureRandom.hex}"
|
||||
|
||||
# Notice that this feature flag is not for `fetch_repository_as_mirror`
|
||||
# as a whole but for the fetching mechanism (file path or gitaly-ssh).
|
||||
url, env = gitaly_migrate(:fetch_internal) do |is_enabled|
|
||||
gitaly_migrate(:remote_fetch_internal_remote) do |is_enabled|
|
||||
if is_enabled
|
||||
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository)
|
||||
[GITALY_INTERNAL_URL, repository.fetch_env]
|
||||
gitaly_remote_client.fetch_internal_remote(repository)
|
||||
else
|
||||
[repository.path, nil]
|
||||
rugged_fetch_repository_as_mirror(repository)
|
||||
end
|
||||
end
|
||||
|
||||
add_remote(remote_name, url, mirror_refmap: :all_refs)
|
||||
fetch_remote(remote_name, env: env)
|
||||
ensure
|
||||
remove_remote(remote_name)
|
||||
end
|
||||
|
||||
def blob_at(sha, path)
|
||||
|
@ -1187,7 +1177,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
# Items should be of format [[commit_id, path], [commit_id1, path1]]
|
||||
def batch_blobs(items, blob_size_limit: nil)
|
||||
def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
|
||||
Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
|
||||
end
|
||||
|
||||
|
@ -1300,6 +1290,42 @@ module Gitlab
|
|||
success || gitlab_projects_error
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def multi_action(
|
||||
user, branch_name:, message:, actions:,
|
||||
author_email: nil, author_name: nil,
|
||||
start_branch_name: nil, start_repository: self)
|
||||
|
||||
OperationService.new(user, self).with_branch(
|
||||
branch_name,
|
||||
start_branch_name: start_branch_name,
|
||||
start_repository: start_repository
|
||||
) do |start_commit|
|
||||
index = Gitlab::Git::Index.new(self)
|
||||
parents = []
|
||||
|
||||
if start_commit
|
||||
index.read_tree(start_commit.rugged_commit.tree)
|
||||
parents = [start_commit.sha]
|
||||
end
|
||||
|
||||
actions.each { |opts| index.apply(opts.delete(:action), opts) }
|
||||
|
||||
committer = user_to_committer(user)
|
||||
author = Gitlab::Git.committer_hash(email: author_email, name: author_name) || committer
|
||||
options = {
|
||||
tree: index.write_tree,
|
||||
message: message,
|
||||
parents: parents,
|
||||
author: author,
|
||||
committer: committer
|
||||
}
|
||||
|
||||
create_commit(options)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def gitaly_repository
|
||||
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
|
||||
end
|
||||
|
@ -2034,6 +2060,16 @@ module Gitlab
|
|||
false
|
||||
end
|
||||
|
||||
def rugged_fetch_repository_as_mirror(repository)
|
||||
remote_name = "tmp-#{SecureRandom.hex}"
|
||||
repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository)
|
||||
|
||||
add_remote(remote_name, GITALY_INTERNAL_URL, mirror_refmap: :all_refs)
|
||||
fetch_remote(remote_name, env: repository.fetch_env)
|
||||
ensure
|
||||
remove_remote(remote_name)
|
||||
end
|
||||
|
||||
def fetch_remote(remote_name = 'origin', env: nil)
|
||||
run_git(['fetch', remote_name], env: env).last.zero?
|
||||
end
|
||||
|
|
|
@ -23,6 +23,19 @@ module Gitlab
|
|||
|
||||
response.result
|
||||
end
|
||||
|
||||
def fetch_internal_remote(repository)
|
||||
request = Gitaly::FetchInternalRemoteRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
remote_repository: repository.gitaly_repository
|
||||
)
|
||||
|
||||
response = GitalyClient.call(@storage, :remote_service,
|
||||
:fetch_internal_remote, request,
|
||||
remote_storage: repository.storage)
|
||||
|
||||
response.result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue