Commit 39e86182 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-01-05

# Conflicts:
#	app/assets/images/icons.json
#	app/assets/images/icons.svg
#	app/assets/javascripts/projects/project_new.js
#	app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
#	app/helpers/branches_helper.rb
#	app/models/concerns/deployment_platform.rb
#	doc/api/boards.md
#	doc/api/settings.md
#	lib/api/entities.rb
#	spec/fixtures/api/schemas/entities/merge_request_widget.json
#	spec/javascripts/lib/utils/text_utility_spec.js

[ci skip]
parents e4f39e49 89f174da
...@@ -747,7 +747,7 @@ GEM ...@@ -747,7 +747,7 @@ GEM
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (2.0.3) redis-rack (2.0.4)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-rails (5.0.2) redis-rails (5.0.2)
......
<<<<<<< HEAD
{"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":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"]}
>>>>>>> upstream/master
This source diff could not be displayed because it is too large. You can view the blob instead.
<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>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="386" height="298" viewBox="0 0 386 298" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M4 51h16v15.997A5.003 5.003 0 0 1 15.003 72H8.997A5.005 5.005 0 0 1 4 66.997V51z"/><rect id="b" width="24" height="10" y="44" rx="3"/></defs><g fill="none" fill-rule="evenodd" transform="translate(0 3)"><g transform="rotate(15 23.151 968.24)"><rect width="53" height="44" fill="#FFF" stroke="#FDE5D8" stroke-width="3" stroke-linecap="round" rx="5"/><path fill="#FDE5D8" d="M29.5 28.3l2.758-3.861c.962-1.347 2.527-1.34 3.484 0l6.516 9.122c.962 1.347.399 2.439-1.252 2.439H17.994c-1.653 0-2.21-1.099-1.252-2.439l6.516-9.122c.962-1.347 2.527-1.34 3.484 0L29.5 28.3z" opacity=".6"/><circle cx="16" cy="16" r="6" fill="#FDB997"/></g><g transform="scale(-1 1) rotate(25 -75.08 -334.15)"><rect width="3" height="11" x="12.45" y="23.45" fill="#6B4FBB" transform="rotate(45 13.95 28.95)" rx="1.5"/><rect width="3" height="14" x="9.45" y="15.45" fill="#6B4FBB" transform="rotate(45 10.95 22.45)" rx="1.5"/><path fill="#FFF" stroke="#E1DCF1" stroke-width="3" d="M16 39.6C6.871 37.747 0 29.676 0 20 0 8.954 8.954 0 20 0s20 8.954 20 20c0 8.955-5.886 16.536-14 19.084v15.91A5.007 5.007 0 0 1 21 60c-2.761 0-5-2.244-5-5.006V39.6zm4-7.6c6.627 0 12-5.373 12-12S26.627 8 20 8 8 13.373 8 20s5.373 12 12 12z"/></g><g transform="scale(1 -1) rotate(-15 -383.616 -172.407)"><path stroke="#FDE5D8" stroke-width="3" d="M1.5 38.5h9V4c0-1.378-1.12-2.5-2.496-2.5H3.996A2.503 2.503 0 0 0 1.5 4v34.5z"/><rect width="2" height="27" x="5" y="7" fill="#FDA77D" opacity=".8" rx="1"/><path stroke="#FDE5D8" stroke-width="3" d="M2.427 41.553h7.146L6 48.699l-3.573-7.146z"/></g><g transform="rotate(-30 420.145 -545.422)"><path fill="#FFF" stroke="#FDE5D8" stroke-width="3" d="M9 3c0-1.657 1.347-3 3-3 1.657 0 3 1.352 3 3v43H9V3z"/><use fill="#FFF" xlink:href="#a"/><path stroke="#FDE5D8" stroke-width="3" d="M5.5 52.5v14.497A3.505 3.505 0 0 0 8.997 70.5h6.006a3.503 3.503 0 0 0 3.497-3.503V52.5h-13z"/><rect width="2" height="14" x="9" y="51" fill="#FDA77D" rx="1"/><rect width="2" height="14" x="13" y="51" fill="#FDA77D" rx="1"/><use fill="#FFF" xlink:href="#b"/><rect width="21" height="7" x="1.5" y="45.5" stroke="#FDE5D8" stroke-width="3" rx="3"/></g><g transform="translate(72 97.488)"><rect width="125" height="160" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><rect width="125" height="160" x="125" fill="#FFF" stroke="#E5E5E5" stroke-width="4" stroke-linecap="round" rx="10"/><path fill="#FFF" stroke="#E5E5E5" stroke-width="4" d="M7 12.008C7 8.69 9.686 6 12.993 6H125v148H12.993C9.683 154 7 151.305 7 147.992V12.008zm236 0C243 8.69 240.314 6 237.007 6H125v148h112.007c3.31 0 5.993-2.695 5.993-6.008V12.008z" stroke-linecap="round"/><rect width="84" height="42" x="142" y="29" stroke="#EEE" stroke-width="4" rx="3"/><rect width="88" height="4" x="141" y="93" fill="#E5E5E5" rx="2"/><rect width="88" height="4" x="141" y="107" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="141" y="121" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="93" fill="#E5E5E5" rx="2"/><rect width="26" height="4" x="22" y="27" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="41" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="55" fill="#BFBFBF" rx="2"/><rect width="56" height="4" x="22" y="69" fill="#E5E5E5" rx="2"/><rect width="36" height="4" x="22" y="107" fill="#E5E5E5" rx="2"/><rect width="56" height="4" x="22" y="121" fill="#BFBFBF" rx="2"/></g><path stroke="#B5A7DD" stroke-width="2.5" d="M23.139 182.922l-1.347-.6a2.004 2.004 0 0 1-1.02-2.64l.815-1.831a1.995 1.995 0 0 1 2.645-1.01l1.308.583a9.959 9.959 0 0 1 2.177-1.876l-.376-1.402a2.004 2.004 0 0 1 1.41-2.455l1.937-.519a1.995 1.995 0 0 1 2.449 1.421l.375 1.402a9.959 9.959 0 0 1 2.824.536l.84-1.158a2.004 2.004 0 0 1 2.796-.448l1.622 1.178a1.995 1.995 0 0 1 .437 2.797l-.867 1.193a9.946 9.946 0 0 1 1.341 2.541l1.461-.05a2.004 2.004 0 0 1 2.075 1.926l.07 2.003a1.995 1.995 0 0 1-1.935 2.067l-1.445.05c-.256.93-.644 1.817-1.15 2.632l.944 1.087a2.004 2.004 0 0 1-.191 2.825l-1.513 1.315a1.995 1.995 0 0 1-2.824-.204l-.963-1.108a10.084 10.084 0 0 1-2.776.744l-.28 1.441a2.004 2.004 0 0 1-2.344 1.588l-1.967-.382a1.995 1.995 0 0 1-1.579-2.35l.275-1.414a10.044 10.044 0 0 1-2.312-1.704l-1.277.678a2.004 2.004 0 0 1-2.709-.822l-.94-1.77a1.995 1.995 0 0 1 .833-2.705l1.29-.687a9.946 9.946 0 0 1-.11-2.872zm10.98 4.93a4 4 0 1 0-2.07-7.727 4 4 0 0 0 2.07 7.728z"/><ellipse cx="197" cy="289.988" fill="#F9F9F9" rx="125" ry="4.5"/><path fill="#6B4FBB" d="M164 100.492a3.002 3.002 0 0 1 3.001-3.004H183a3.006 3.006 0 0 1 3.001 3.004v34.988c0 2.213-1.45 2.954-3.24 1.651l-7.76-5.643-7.76 5.643c-1.789 1.302-3.24.566-3.24-1.651v-34.988z"/><g opacity=".2"><path fill="#FC8A51" d="M5.747 234.768l-2.688 1.114c-1.017.422-1.803-.134-1.754-1.228l.128-2.907-1.115-2.688c-.422-1.017.135-1.803 1.229-1.754l2.907.128 2.687-1.115c1.018-.422 1.803.135 1.755 1.229l-.128 2.907 1.114 2.687c.422 1.018-.134 1.803-1.228 1.755l-2.907-.128zM191.564 37.953l-3.72.164c-1.326.059-1.992-.88-1.48-2.115l1.426-3.438-.164-3.72c-.059-1.326.88-1.992 2.115-1.48l3.438 1.426 3.72-.164c1.326-.059 1.992.88 1.48 2.114l-1.426 3.44.164 3.719c.059 1.326-.88 1.992-2.114 1.48l-3.44-1.426z"/><path fill="#6B4FBB" d="M348.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775zm9.261 164.735l-2.907-.125c-1.1-.048-1.577-.884-1.07-1.855l1.344-2.58.126-2.908c.047-1.1.883-1.577 1.855-1.07l2.58 1.344 2.907.126c1.1.047 1.577.883 1.07 1.855l-1.344 2.58-.125 2.907c-.048 1.1-.884 1.577-1.856 1.07l-2.58-1.344zM88.789 75.876l-1.967-2.144c-.744-.812-.49-1.74.555-2.07l2.775-.873 2.144-1.967c.812-.744 1.74-.49 2.07.555l.873 2.775 1.967 2.144c.744.812.49 1.74-.555 2.07l-2.775.873-2.144 1.967c-.812.745-1.74.49-2.07-.555l-.873-2.775z"/></g></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg>
\ No newline at end of file
...@@ -74,6 +74,18 @@ const gfmRules = { ...@@ -74,6 +74,18 @@ const gfmRules = {
return `![${el.dataset.title}](${el.getAttribute('src')})`; return `![${el.dataset.title}](${el.getAttribute('src')})`;
}, },
}, },
MermaidFilter: {
'svg.mermaid'(el, text) {
const sourceEl = el.querySelector('text.source');
if (!sourceEl) return false;
return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
},
'svg.mermaid style, svg.mermaid g'(el, text) {
// We don't want to include the content of these elements in the copied text.
return '';
},
},
MathFilter: { MathFilter: {
'pre.code.math[data-math-style=display]'(el, text) { 'pre.code.math[data-math-style=display]'(el, text) {
return `\`\`\`math\n${text.trim()}\n\`\`\``; return `\`\`\`math\n${text.trim()}\n\`\`\``;
......
...@@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown { ...@@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown {
let target; let target;
let value; let value;
if (event.srcElement === this.branchInput) { if (event.target === this.branchInput) {
target = 'branch'; target = 'branch';
value = this.branchInput.value; value = this.branchInput.value;
} else if (event.srcElement === this.refInput) { } else if (event.target === this.refInput) {
target = 'ref'; target = 'ref';
value = event.srcElement.value.slice(0, event.srcElement.selectionStart) + value = event.target.value.slice(0, event.target.selectionStart) +
event.srcElement.value.slice(event.srcElement.selectionEnd); event.target.value.slice(event.target.selectionEnd);
} else { } else {
return false; return false;
} }
......
...@@ -45,11 +45,9 @@ export default { ...@@ -45,11 +45,9 @@ export default {
onLeaveGroup() { onLeaveGroup() {
this.modalStatus = true; this.modalStatus = true;
}, },
leaveGroup(leaveConfirmed) { leaveGroup() {
this.modalStatus = false; this.modalStatus = false;
if (leaveConfirmed) { eventHub.$emit('leaveGroup', this.group, this.parentGroup);
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
}
}, },
}, },
}; };
......
...@@ -42,28 +42,28 @@ export default { ...@@ -42,28 +42,28 @@ export default {
v-if="isGroup" v-if="isGroup"
css-class="number-subgroups" css-class="number-subgroups"
icon-name="folder" icon-name="folder"
:title="s__('Subgroups')" :title="__('Subgroups')"
:value=item.subgroupCount :value="item.subgroupCount"
/> />
<item-stats-value <item-stats-value
v-if="isGroup" v-if="isGroup"
css-class="number-projects" css-class="number-projects"
icon-name="bookmark" icon-name="bookmark"
:title="s__('Projects')" :title="__('Projects')"
:value=item.projectCount :value="item.projectCount"
/> />
<item-stats-value <item-stats-value
v-if="isGroup" v-if="isGroup"
css-class="number-users" css-class="number-users"
icon-name="users" icon-name="users"
:title="s__('Members')" :title="__('Members')"
:value=item.memberCount :value="item.memberCount"
/> />
<item-stats-value <item-stats-value
v-if="isProject" v-if="isProject"
css-class="project-stars" css-class="project-stars"
icon-name="star" icon-name="star"
:value=item.starCount :value="item.starCount"
/> />
<item-stats-value <item-stats-value
css-class="item-visibility" css-class="item-visibility"
......
...@@ -9,6 +9,12 @@ import repoPreview from './repo_preview.vue'; ...@@ -9,6 +9,12 @@ import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([ ...mapState([
'currentBlobView', 'currentBlobView',
...@@ -64,7 +70,23 @@ export default { ...@@ -64,7 +70,23 @@ export default {
<template <template
v-else> v-else>
<div class="ide-empty-state"> <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> </div>
</template> </template>
</div> </div>
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import repoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue'; import repoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import { treeList } from '../stores/utils'; import { treeList } from '../stores/utils';
export default { export default {
components: { components: {
'repo-previous-directory': RepoPreviousDirectory, repoPreviousDirectory,
'repo-file': RepoFile, repoFile,
'repo-loading-file': RepoLoadingFile, skeletonLoadingContainer,
}, },
props: { props: {
treeId: { treeId: {
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading', 'trees',
'isRoot', 'isRoot',
]), ]),
...mapState({ ...mapState({
...@@ -34,7 +34,10 @@ export default { ...@@ -34,7 +34,10 @@ export default {
return !this.isRoot && this.fetchedList.length; return !this.isRoot && this.fetchedList.length;
}, },
showLoading() { showLoading() {
return this.loading; if (this.trees[this.treeId]) {
return this.trees[this.treeId].loading;
}
return true;
}, },
}, },
}; };
...@@ -49,11 +52,13 @@ export default { ...@@ -49,11 +52,13 @@ export default {
<repo-previous-directory <repo-previous-directory
v-if="hasPreviousDirectory" v-if="hasPreviousDirectory"
/> />
<repo-loading-file <div
class="multi-file-loading-container"
v-if="showLoading" v-if="showLoading"
v-for="n in 5" v-for="n in 3"
:key="n" :key="n">
/> <skeleton-loading-container/>
</div>
<repo-file <repo-file
v-for="file in fetchedList" v-for="file in fetchedList"
:key="file.key" :key="file.key"
......
...@@ -3,6 +3,7 @@ import { mapState, mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue'; import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue'; import panelResizer from '../../vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default { export default {
data() { data() {
...@@ -14,9 +15,11 @@ export default { ...@@ -14,9 +15,11 @@ export default {
projectTree, projectTree,
icon, icon,
panelResizer, panelResizer,
skeletonLoadingContainer,
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading',
'projects', 'projects',
'leftPanelCollapsed', 'leftPanelCollapsed',
]), ]),
...@@ -32,6 +35,9 @@ export default { ...@@ -32,6 +35,9 @@ export default {
} }
return {}; return {};
}, },
showLoading() {
return this.loading;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -63,6 +69,13 @@ export default { ...@@ -63,6 +69,13 @@ export default {
:style="panelStyle" :style="panelStyle"
> >
<div class="multi-file-commit-panel-inner"> <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 <project-tree
v-for="(project, index) in projects" v-for="(project, index) in projects"
:key="project.id" :key="project.id"
......
...@@ -32,10 +32,10 @@ ...@@ -32,10 +32,10 @@
methods: { methods: {
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
this.toggleModalOpen(); this.openModal = true;
}, },
toggleModalOpen() { hideModal() {
this.openModal = !this.openModal; this.openModal = false;
}, },
}, },
}; };
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
:branch-id="branch" :branch-id="branch"
:path="path" :path="path"
:parent="parent" :parent="parent"
@toggle="toggleModalOpen" @hide="hideModal"
/> />
</div> </div>
</template> </template>
...@@ -43,10 +43,10 @@ ...@@ -43,10 +43,10 @@
type: this.type, type: this.type,
}); });
this.toggleModalOpen(); this.hideModal();
}, },
toggleModalOpen() { hideModal() {
this.$emit('toggle'); this.$emit('hide');
}, },
}, },
computed: { computed: {
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
:title="modalTitle" :title="modalTitle"
:primary-button-label="buttonLabel" :primary-button-label="buttonLabel"
kind="success" kind="success"
@toggle="toggleModalOpen" @cancel="hideModal"
@submit="createEntryInStore" @submit="createEntryInStore"
> >
<form <form
......
...@@ -110,7 +110,7 @@ export default { ...@@ -110,7 +110,7 @@ export default {
kind="primary" kind="primary"
:title="__('Branch has changed')" :title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')" :text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@toggle="showNewBranchModal = false" @cancel="showNewBranchModal = false"
@submit="makeCommit(true)" @submit="makeCommit(true)"
/> />
<commit-files-list <commit-files-list
......
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
kind="warning" kind="warning"
:title="__('Are you sure?')" :title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')" :text="__('Are you sure you want to discard your changes?')"
@toggle="closeDiscardPopup" @cancel="closeDiscardPopup"
@submit="toggleEditMode(true)" @submit="toggleEditMode(true)"
/> />
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import ide from './components/ide.vue'; import ide from './components/ide.vue';
import store from './stores'; import store from './stores';
import router from './ide_router'; import router from './ide_router';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import ContextualSidebar from '../contextual_sidebar';
function initIde(el) { function initIde(el) {
if (!el) return null; if (!el) return null;
...@@ -18,30 +14,13 @@ function initIde(el) { ...@@ -18,30 +14,13 @@ function initIde(el) {
components: { components: {
ide, ide,
}, },
methods: { render(createElement) {
...mapActions([ return createElement('ide', {
'setInitialData', props: {
]), emptyStateSvgPath: el.dataset.emptyStateSvgPath,
},
created() {
const data = el.dataset;
this.setInitialData({
endpoints: {
rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl,
rootUrl: data.rootUrl,
}, },
canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
path: data.currentPath,
isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root),
}); });
}, },
render(createElement) {
return createElement('ide');
},
}); });
} }
...@@ -50,6 +29,3 @@ const ideElement = document.getElementById('ide'); ...@@ -50,6 +29,3 @@ const ideElement = document.getElementById('ide');
Vue.use(Translate); Vue.use(Translate);
initIde(ideElement); initIde(ideElement);
const contextualSidebar = new ContextualSidebar();
contextualSidebar.bindEvents();
...@@ -8,9 +8,11 @@ export const getProjectData = ( ...@@ -8,9 +8,11 @@ export const getProjectData = (
{ namespace, projectId, force = false } = {}, { namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => { ) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, state);
service.getProjectData(namespace, projectId) service.getProjectData(namespace, projectId)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then((data) => {
commit(types.TOGGLE_LOADING, state);
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data); resolve(data);
......
<script> <script>
import modal from '../../../vue_shared/components/modal.vue'; import modal from '~/vue_shared/components/modal.vue';
import { __, s__, sprintf } from '../../../locale'; import { __, s__, sprintf } from '~/locale';
import csrf from '../../../lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
export default { export default {
props: { props: {
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
return { return {
enteredPassword: '', enteredPassword: '',
enteredUsername: '', enteredUsername: '',
isOpen: false,
}; };
}, },
components: { components: {
...@@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
return this.enteredUsername === this.username; return this.enteredUsername === this.username;
}, },
onSubmit(status) { onSubmit() {
if (status) { this.$refs.form.submit();
if (!this.canSubmit()) {
return;
}
this.$refs.form.submit();
}
this.toggleOpen(false);
},
toggleOpen(isOpen) {
this.isOpen = isOpen;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <modal
<modal id="delete-account-modal"
v-if="isOpen" :title="s__('Profiles|Delete your account?')"
:title="s__('Profiles|Delete your account?')" :text="text"
:text="text" kind="danger"
:kind="`danger ${!canSubmit() && 'disabled'}`" :primary-button-label="s__('Profiles|Delete account')"
:primary-button-label="s__('Profiles|Delete account')" @submit="onSubmit"
@toggle="toggleOpen" :submit-disabled="!canSubmit()">
@submit="onSubmit">
<template slot="body" slot-scope="props">
<p v-html="props.text"></p>
<form <template slot="body" slot-scope="props">
ref="form" <p v-html="props.text"></p>
:action="actionUrl"
method="post">
<input <form
type="hidden" ref="form"
name="_method" :action="actionUrl"
value="delete" /> method="post">
<input
type="hidden"
name="authenticity_token"
:value="csrfToken" />
<p id="input-label" v-html="inputLabel"></p> <input
type="hidden"
name="_method"
value="delete" />
<input
type="hidden"
name="authenticity_token"
:value="csrfToken" />
<input <p id="input-label" v-html="inputLabel"></p>
v-if="confirmWithPassword"
name="password"
class="form-control"
type="password"
v-model="enteredPassword"
aria-labelledby="input-label" />
<input
v-else
name="username"
class="form-control"
type="text"
v-model="enteredUsername"
aria-labelledby="input-label" />
</form>
</template>
</modal> <input
v-if="confirmWithPassword"
name="password"
class="form-control"
type="password"
v-model="enteredPassword"
aria-labelledby="input-label" />
<input
v-else
name="username"
class="form-control"
type="text"
v-model="enteredUsername"
aria-labelledby="input-label" />
</form>
</template>
<button </modal>
type="button"
class="btn btn-danger"
@click="toggleOpen(true)">
{{ s__('Profiles|Delete account') }}
</button>
</div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import deleteAccountModal from './components/delete_account_modal.vue'; import deleteAccountModal from './components/delete_account_modal.vue';
Vue.use(Translate);
const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountModalEl = document.getElementById('delete-account-modal'); const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
...@@ -9,6 +14,9 @@ new Vue({ ...@@ -9,6 +14,9 @@ new Vue({
components: { components: {
deleteAccountModal, deleteAccountModal,
}, },
mounted() {
deleteAccountButton.classList.remove('disabled');
},
render(createElement) { render(createElement) {
return createElement('delete-account-modal', { return createElement('delete-account-modal', {
props: { props: {
......
...@@ -98,12 +98,15 @@ const bindEvents = () => { ...@@ -98,12 +98,15 @@ const bindEvents = () => {
}); });
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
<<<<<<< HEAD
$('.import_git').on('click', () => { $('.import_git').on('click', () => {
const $projectMirror = $('#project_mirror'); const $projectMirror = $('#project_mirror');
$projectMirror.attr('disabled', !$projectMirror.attr('disabled')); $projectMirror.attr('disabled', !$projectMirror.attr('disabled'));
}); });
=======
>>>>>>> upstream/master
}; };
document.addEventListener('DOMContentLoaded', bindEvents); document.addEventListener('DOMContentLoaded', bindEvents);
......
...@@ -24,7 +24,25 @@ export default function renderMermaid($els) { ...@@ -24,7 +24,25 @@ export default function renderMermaid($els) {
}); });
$els.each((i, el) => { $els.each((i, el) => {
mermaid.init(undefined, el); const source = el.textContent;
mermaid.init(undefined, el, (id) => {
const svg = document.getElementById(id);
svg.classList.add('mermaid');
// pre > code > svg
svg.closest('pre').replaceWith(svg);
// We need to add the original source into the DOM to allow Copy-as-GFM
// to access it.
const sourceEl = document.createElement('text');
sourceEl.classList.add('source');
sourceEl.setAttribute('display', 'none');
sourceEl.textContent = source;
svg.appendChild(sourceEl);
});
}); });
}).catch((err) => { }).catch((err) => {
Flash(`Can't load mermaid module: ${err}`); Flash(`Can't load mermaid module: ${err}`);
......
...@@ -81,7 +81,10 @@ export default { ...@@ -81,7 +81,10 @@ export default {
statusPath: store.statusPath, statusPath: store.statusPath,
mergeActionsContentPath: store.mergeActionsContentPath, mergeActionsContentPath: store.mergeActionsContentPath,
rebasePath: store.rebasePath, rebasePath: store.rebasePath,
<<<<<<< HEAD
approvalsPath: store.approvalsPath, approvalsPath: store.approvalsPath,
=======
>>>>>>> upstream/master
}; };
return new MRWidgetService(endpoints); return new MRWidgetService(endpoints);
}, },
......
...@@ -3,6 +3,10 @@ export default { ...@@ -3,6 +3,10 @@ export default {
name: 'modal', name: 'modal',
props: { props: {
id: {
type: String,
required: false,
},
title: { title: {
type: String, type: String,
required: false, required: false,
...@@ -62,11 +66,11 @@ export default { ...@@ -62,11 +66,11 @@ export default {
}, },
methods: { methods: {
close() { emitCancel(event) {
this.$emit('toggle', false); this.$emit('cancel', event);
}, },
emitSubmit(status) { emitSubmit(event) {
this.$emit('submit', status); this.$emit('submit', event);
}, },
}, },
}; };
...@@ -75,7 +79,9 @@ export default { ...@@ -75,7 +79,9 @@ export default {
<template> <template>
<div class="modal-open"> <div class="modal-open">
<div <div
class="modal show" :id="id"
class="modal"
:class="id ? '' : 'show'"
role="dialog" role="dialog"
tabindex="-1" tabindex="-1"
> >
...@@ -93,7 +99,8 @@ export default { ...@@ -93,7 +99,8 @@ export default {
<button <button
type="button" type="button"
class="close pull-right" class="close pull-right"
@click="close" @click="emitCancel($event)"
data-dismiss="modal"
aria-label="Close" aria-label="Close"
> >
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
...@@ -110,7 +117,8 @@ export default { ...@@ -110,7 +117,8 @@ export default {
type="button" type="button"
class="btn pull-left" class="btn pull-left"
:class="btnCancelKindClass" :class="btnCancelKindClass"
@click="close"> @click="emitCancel($event)"
data-dismiss="modal">
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<button <button
...@@ -119,13 +127,17 @@ export default { ...@@ -119,13 +127,17 @@ export default {
class="btn pull-right js-primary-button" class="btn pull-right js-primary-button"
:disabled="submitDisabled" :disabled="submitDisabled"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit($event)"
data-dismiss="modal">
{{ primaryButtonLabel }} {{ primaryButtonLabel }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-backdrop fade in" /> <div
v-if="!id"
class="modal-backdrop fade in">
</div>
</div> </div>
</template> </template>
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
class="recaptcha-modal js-recaptcha-modal" class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true" :hide-footer="true"
:title="__('Please solve the reCAPTCHA')" :title="__('Please solve the reCAPTCHA')"
@toggle="close" @cancel="close"
> >
<div slot="body"> <div slot="body">
<p> <p>
......
...@@ -97,6 +97,19 @@ ...@@ -97,6 +97,19 @@
padding: 6px 12px; 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 { table.table tr td.multi-file-table-name {
width: 350px; width: 350px;
padding: 6px 12px; padding: 6px 12px;
......
...@@ -5,8 +5,6 @@ module IssuesAction ...@@ -5,8 +5,6 @@ module IssuesAction
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues def issues
@finder_type = IssuesFinder @finder_type = IssuesFinder
@label = finder.labels.first
@issues = issuables_collection @issues = issuables_collection
.non_archived .non_archived
.page(params[:page]) .page(params[:page])
......
...@@ -5,7 +5,6 @@ module MergeRequestsAction ...@@ -5,7 +5,6 @@ module MergeRequestsAction
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def merge_requests def merge_requests
@finder_type = MergeRequestsFinder @finder_type = MergeRequestsFinder
@label = finder.labels.first
@merge_requests = issuables_collection.page(params[:page]) @merge_requests = issuables_collection.page(params[:page])
......
...@@ -356,7 +356,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -356,7 +356,7 @@ class ProjectsController < Projects::ApplicationController
end end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? && project.repo project.repository_exists? && !project.empty_repo?
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
project.repository.expire_exists_cache project.repository.expire_exists_cache
......
...@@ -367,19 +367,14 @@ class IssuableFinder ...@@ -367,19 +367,14 @@ class IssuableFinder
end end
def by_label(items) def by_label(items)
if labels? return items unless labels?
items =
if filter_by_no_label? if filter_by_no_label?
items = items.without_label items.without_label
else else
items = items.with_label(label_names, params[:sort]) items.with_label(label_names, params[:sort])
items_projects = projects(items)
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 end
end
items items
end end
......
...@@ -24,6 +24,7 @@ module BranchesHelper ...@@ -24,6 +24,7 @@ module BranchesHelper
ProtectedBranch.protected?(project, branch.name) ProtectedBranch.protected?(project, branch.name)
end end
<<<<<<< HEAD
# Returns a hash were keys are types of access levels (user, role), and # Returns a hash were keys are types of access levels (user, role), and
# values are the number of access levels of the particular type. # values are the number of access levels of the particular type.
def access_level_frequencies(access_levels) def access_level_frequencies(access_levels)
...@@ -48,6 +49,13 @@ module BranchesHelper ...@@ -48,6 +49,13 @@ module BranchesHelper
else else
{ id: level.id, type: level.type, access_level: level.access_level } { id: level.id, type: level.type, access_level: level.access_level }
end end
=======
def diverging_count_label(count)
if count >= Repository::MAX_DIVERGING_COUNT
"#{Repository::MAX_DIVERGING_COUNT - 1}+"
else
count.to_s
>>>>>>> upstream/master
end end
end end
end end
module DeploymentPlatform module DeploymentPlatform
<<<<<<< HEAD
def deployment_platform(environment: nil) def deployment_platform(environment: nil)
=======
def deployment_platform
>>>>>>> upstream/master
@deployment_platform ||= @deployment_platform ||=
find_cluster_platform_kubernetes || find_cluster_platform_kubernetes ||
find_kubernetes_service_integration || find_kubernetes_service_integration ||
......
...@@ -997,10 +997,6 @@ class Project < ActiveRecord::Base ...@@ -997,10 +997,6 @@ class Project < ActiveRecord::Base
false false
end end
def repo
repository.rugged
end
def url_to_repo def url_to_repo
gitlab_shell.url_to_repo(full_path) gitlab_shell.url_to_repo(full_path)
end end
...@@ -1444,7 +1440,7 @@ class Project < ActiveRecord::Base ...@@ -1444,7 +1440,7 @@ class Project < ActiveRecord::Base
# We'd need to keep track of project full path otherwise directory tree # We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using # created with hashed storage enabled cannot be usefully imported using
# the import rake task. # the import rake task.
repo.config['gitlab.fullpath'] = gl_full_path repository.rugged.config['gitlab.fullpath'] = gl_full_path
rescue Gitlab::Git::Repository::NoRepository => e rescue Gitlab::Git::Repository::NoRepository => e
Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil nil
......
...@@ -6,6 +6,7 @@ class Repository ...@@ -6,6 +6,7 @@ class Repository
REF_MERGE_REQUEST = 'merge-requests'.freeze REF_MERGE_REQUEST = 'merge-requests'.freeze
REF_KEEP_AROUND = 'keep-around'.freeze REF_KEEP_AROUND = 'keep-around'.freeze
REF_ENVIRONMENTS = 'environments'.freeze REF_ENVIRONMENTS = 'environments'.freeze
MAX_DIVERGING_COUNT = 1000
RESERVED_REFS_NAMES = %W[ RESERVED_REFS_NAMES = %W[
heads heads
...@@ -285,11 +286,12 @@ class Repository ...@@ -285,11 +286,12 @@ class Repository
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather # Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes # than SHA-1 hashes
number_commits_behind = raw_repository number_commits_behind, number_commits_ahead =
.count_commits_between(branch.dereferenced_target.sha, root_ref_hash) raw_repository.count_commits_between(
root_ref_hash,
number_commits_ahead = raw_repository branch.dereferenced_target.sha,
.count_commits_between(root_ref_hash, branch.dereferenced_target.sha) left_right: true,
max_count: MAX_DIVERGING_COUNT)
{ behind: number_commits_behind, ahead: number_commits_ahead } { behind: number_commits_behind, ahead: number_commits_ahead }
end end
......
...@@ -42,6 +42,16 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -42,6 +42,16 @@ class MergeRequestWidgetEntity < IssuableEntity
MergeRequestMetricsEntity.new(metrics).as_json MergeRequestMetricsEntity.new(metrics).as_json
end end
expose :rebase_commit_sha
expose :rebase_in_progress?, as: :rebase_in_progress
expose :can_push_to_source_branch do |merge_request|
presenter(merge_request).can_push_to_source_branch?
end
expose :rebase_path do |merge_request|
presenter(merge_request).rebase_path
end
# User entities # User entities
expose :merge_user, using: UserEntity expose :merge_user, using: UserEntity
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.ide-flash-container.flash-container .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 .text-center
= icon('spinner spin 2x') = icon('spinner spin 2x')
%h2.clgray= _('IDE Loading ...') %h2.clgray= _('Loading the GitLab IDE...')
...@@ -49,6 +49,12 @@ ...@@ -49,6 +49,12 @@
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects 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? - if current_user.admin? || Gitlab::Sherlock.enabled?
%li.line-separator.hidden-xs %li.line-separator.hidden-xs
- if current_user.admin? - if current_user.admin?
......
...@@ -84,11 +84,13 @@ ...@@ -84,11 +84,13 @@
= s_('Profiles|Deleting an account has the following effects:') = s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user = render 'users/deletion_guidance', user: current_user
%button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
target: '#delete-account-modal' } }
= s_('Profiles|Delete account')
#delete-account-modal{ data: { action_url: user_registration_path, #delete-account-modal{ data: { action_url: user_registration_path,
confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
username: current_user.username } } username: current_user.username } }
%button.btn.btn-danger.disabled
= s_('Profiles|Delete account')
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
%p %p
......
...@@ -72,16 +72,16 @@ ...@@ -72,16 +72,16 @@
= icon("trash-o") = icon("trash-o")
- if branch.name != @repository.root_ref - if branch.name != @repository.root_ref
.divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind, .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref, default_branch: @repository.root_ref,
number_commits_ahead: number_commits_ahead } } number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side .graph-side
.bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
%span.count.count-behind= number_commits_behind %span.count.count-behind= diverging_count_label(number_commits_behind)
.graph-separator .graph-separator
.graph-side .graph-side
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
%span.count.count-ahead= number_commits_ahead %span.count.count-ahead= diverging_count_label(number_commits_ahead)
- if commit - if commit
......
...@@ -11,5 +11,5 @@ ...@@ -11,5 +11,5 @@
%label.text-danger %label.text-danger
= s_('ClusterIntegration|Remove cluster integration') = s_('ClusterIntegration|Remove cluster integration')
%p %p
= s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.') = s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.")
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"}) = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
%h4= s_('ClusterIntegration|Enable cluster integration') %h4= s_('ClusterIntegration|Cluster integration')
.settings-content
.settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine') = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
%p.js-error-reason %p.js-error-reason
...@@ -11,11 +11,4 @@ ...@@ -11,11 +11,4 @@
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details') = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
%p %p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
- if @cluster.enabled?
- if can?(current_user, :update_cluster, @cluster)
= s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
- else
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.table-mobile-content .table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.environment_scope .table-mobile-content= cluster.environment_scope
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
......
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group.append-bottom-20 .form-group.append-bottom-20
%h5= s_('ClusterIntegration|Integration status')
%p
- if @cluster.enabled?
- if can?(current_user, :update_cluster, @cluster)
= s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
- else
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
%label.append-bottom-10 %label.append-bottom-10
= field.hidden_field :enabled, { class: 'js-toggle-input'} = field.hidden_field :enabled, { class: 'js-toggle-input'}
...@@ -12,6 +21,13 @@ ...@@ -12,6 +21,13 @@
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- if can?(current_user, :update_cluster, @cluster) .form-group
.form-group %h5= s_('ClusterIntegration|Environment scope')
= field.submit _('Save'), class: 'btn btn-success' %p
= s_("ClusterIntegration|Choose which of your project's environments will use this cluster.")
= link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
...@@ -9,10 +9,6 @@ ...@@ -9,10 +9,6 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group .form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Cluster") = s_("ClusterIntegration|Cluster")
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment pattern") = s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace") = s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" } .table-section.section-10{ role: "rowheader" }
......
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
.js-cluster-application-notice .js-cluster-application-notice
.flash-container .flash-container
%section.settings.no-animate.expanded %section.settings.no-animate.expanded#cluster-integration
= render 'banner' = render 'banner'
= render 'enabled' = render 'integration_form'
.cluster-applications-table#js-cluster-applications .cluster-applications-table#js-cluster-applications
...@@ -41,6 +41,6 @@ ...@@ -41,6 +41,6 @@
%h4= _('Advanced settings') %h4= _('Advanced settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|Manage cluster integration on your GitLab project') %p= s_("ClusterIntegration|Advanced options on this cluster's integration")
.settings-content .settings-content
= render 'advanced_settings' = render 'advanced_settings'
...@@ -4,10 +4,6 @@ ...@@ -4,10 +4,6 @@
= field.label :name, s_('ClusterIntegration|Cluster name') = field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group .form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
......
---
title: Fix gitlab-rake gitlab:import:repos import schedule
merge_request: 15931
author:
type: fixed
---
title: Allow user to rebase merge requests.
merge_request:
author:
type: added
---
title: Improve the performance for counting diverging commits. Show 999+
if it is more than 1000 commits
merge_request: 15963
author:
type: performance
---
title: Force Auto DevOps kubectl version to 1.8.6
merge_request: 16218
author:
type: fixed
---
title: Expose project_id on /api/v4/pages/domains
merge_request: 16200
author: Luc Didry
type: changed
---
title: Add online and status attribute to runner api entity
merge_request: 11750
author:
type: added
---
title: Fix timeout when filtering issues by label
merge_request:
author:
type: performance
---
title: 'API: get participants from merge_requests & issues'
merge_request: 16187
author: Brent Greeff
type: added
---
title: Update redis-rack to 2.0.4
merge_request:
author:
type: other
---
title: Add id to modal.vue to support data-toggle="modal"
merge_request: 16189
author:
type: other
# Creates a project with labeled issues for a user.
# Run this single seed file using: rake db:seed_fu FILTER=labeled USER_ID=74.
# If an USER_ID is not provided it will use the last created user.
require './spec/support/sidekiq'
class Gitlab::Seeder::LabeledIssues
include ::Gitlab::Utils
def initialize(user)
@user = user
end
def seed!
Sidekiq::Testing.inline! do
group = create_group
puts '.'
create_projects(group)
puts '.'
create_labels(group)
puts '.'
create_issues(group)
puts '.'
end
print '.'
end
private
def create_group
group_name = "group_of_#{@user.name}#{SecureRandom.hex(4)}"
group = Group.new(
name: group_name,
path: group_name,
description: FFaker::Lorem.sentence
)
group.save
group.add_owner(@user)
group
end
def create_projects(group)
5.times do
project_name = "project_#{SecureRandom.hex(6)}"
params = {
namespace_id: group.id,
name: project_name,
description: FFaker::Lorem.sentence,
visibility_level: Gitlab::VisibilityLevel.values.sample
}
Projects::CreateService.new(@user, params).execute
end
end
def create_labels(group)
group.projects.each do |project|
5.times do
label_title = FFaker::Vehicle.model
Labels::CreateService.new(title: label_title).execute(project: project)
end
end
10.times do
label_title = FFaker::Product.brand
Labels::CreateService.new(title: label_title).execute(group: group)
end
end
def create_issues(group)
# Get only group labels
group_labels =
LabelsFinder.new(@user, group_id: group.id).execute.where.not(group_id: nil)
group.projects.each do |project|
label_ids = project.labels.pluck(:id).sample(5)
label_ids.push(*group.labels.sample(4))
50.times do
issue_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentence,
state: 'opened',
label_ids: label_ids
}
Issues::CreateService.new(project, @user, issue_params).execute if project.project_feature.present?
end
end
end
end
Gitlab::Seeder.quiet do
user_id = ENV['USER_ID']
user =
if user_id.present?
User.find(user_id)
else
User.last
end
Gitlab::Seeder::LabeledIssues.new(user).seed!
end
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :merge_requests, :rebase_commit_sha, :string
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171229225929) do ActiveRecord::Schema.define(version: 20171230123729) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1449,6 +1449,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do ...@@ -1449,6 +1449,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do
t.string "merge_jid" t.string "merge_jid"
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id" t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
...@@ -28,19 +28,25 @@ exactly which repositories are causing the trouble. ...@@ -28,19 +28,25 @@ exactly which repositories are causing the trouble.
### Check all GitLab repositories ### Check all GitLab repositories
>**Note:**
>
> - `gitlab:repo:check` has been deprecated in favor of `gitlab:git:fsck`
> - [Deprecated][ce-15931] in GitLab 10.4.
> - `gitlab:repo:check` will be removed in the future. [Removal issue][ce-41699]
This task loops through all repositories on the GitLab server and runs the This task loops through all repositories on the GitLab server and runs the
3 integrity checks described previously. 3 integrity checks described previously.
**Omnibus Installation** **Omnibus Installation**
``` ```
sudo gitlab-rake gitlab:repo:check sudo gitlab-rake gitlab:git:fsck
``` ```
**Source Installation** **Source Installation**
```bash ```bash
sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:git:fsck RAILS_ENV=production
``` ```
### Check repositories for a specific user ### Check repositories for a specific user
...@@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials ...@@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials
(if configured) and will list a sample of LDAP users. This task is also (if configured) and will list a sample of LDAP users. This task is also
executed as part of the `gitlab:check` task, but can run independently. executed as part of the `gitlab:check` task, but can run independently.
See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details. See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details.
[ce-15931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15931
[ce-41699]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41699
...@@ -141,6 +141,7 @@ Example response: ...@@ -141,6 +141,7 @@ Example response:
} }
``` ```
<<<<<<< HEAD
## Create a board (EES-Only) ## Create a board (EES-Only)
Creates a board. Creates a board.
...@@ -226,6 +227,8 @@ DELETE /projects/:id/boards/:board_id ...@@ -226,6 +227,8 @@ DELETE /projects/:id/boards/:board_id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1 curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
``` ```
=======
>>>>>>> upstream/master
## List board lists ## List board lists
Get a list of the board's lists. Get a list of the board's lists.
......
...@@ -1134,6 +1134,45 @@ Example response: ...@@ -1134,6 +1134,45 @@ Example response:
``` ```
## Participants on issues
```
GET /projects/:id/issues/:issue_iid/participants
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/participants
```
Example response:
```json
[
{
"id": 1,
"name": "John Doe1",
"username": "user1",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user1"
},
{
"id": 5,
"name": "John Doe5",
"username": "user5",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80&d=identicon",
"web_url": "http://localhost/user5"
}
]
```
## Comments on issues ## Comments on issues
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
......
...@@ -312,6 +312,41 @@ Parameters: ...@@ -312,6 +312,41 @@ Parameters:
} }
``` ```
## Get single MR participants
Get a list of merge request participants.
```
GET /projects/:id/merge_requests/:merge_request_iid/participants
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
```json
[
{
"id": 1,
"name": "John Doe1",
"username": "user1",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user1"
},
{
"id": 2,
"name": "John Doe2",
"username": "user2",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon",
"web_url": "http://localhost/user2"
},
]
```
## Get single MR commits ## Get single MR commits
Get a list of merge request commits. Get a list of merge request commits.
......
...@@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a ...@@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
{ {
"domain": "ssl.domain.example", "domain": "ssl.domain.example",
"url": "https://ssl.domain.example", "url": "https://ssl.domain.example",
"project_id": 1337,
"certificate": { "certificate": {
"expired": false, "expired": false,
"expiration": "2020-04-12T14:32:00.000Z" "expiration": "2020-04-12T14:32:00.000Z"
......
...@@ -30,14 +30,18 @@ Example response: ...@@ -30,14 +30,18 @@ Example response:
"description": "test-1-20150125", "description": "test-1-20150125",
"id": 6, "id": 6,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": true,
"status": "online"
}, },
{ {
"active": true, "active": true,
"description": "test-2-20150125", "description": "test-2-20150125",
"id": 8, "id": 8,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": false,
"status": "offline"
} }
] ]
``` ```
...@@ -69,28 +73,36 @@ Example response: ...@@ -69,28 +73,36 @@ Example response:
"description": "shared-runner-1", "description": "shared-runner-1",
"id": 1, "id": 1,
"is_shared": true, "is_shared": true,
"name": null "name": null,
"online": true,
"status": "online"
}, },
{ {
"active": true, "active": true,
"description": "shared-runner-2", "description": "shared-runner-2",
"id": 3, "id": 3,
"is_shared": true, "is_shared": true,
"name": null "name": null,
"online": false
"status": "offline"
}, },
{ {
"active": true, "active": true,
"description": "test-1-20150125", "description": "test-1-20150125",
"id": 6, "id": 6,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": true
"status": "paused"
}, },
{ {
"active": true, "active": true,
"description": "test-2-20150125", "description": "test-2-20150125",
"id": 8, "id": 8,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": false,
"status": "offline"
} }
] ]
``` ```
...@@ -122,6 +134,8 @@ Example response: ...@@ -122,6 +134,8 @@ Example response:
"is_shared": false, "is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z", "contacted_at": "2016-01-25T16:39:48.066Z",
"name": null, "name": null,
"online": true,
"status": "online",
"platform": null, "platform": null,
"projects": [ "projects": [
{ {
...@@ -176,6 +190,8 @@ Example response: ...@@ -176,6 +190,8 @@ Example response:
"is_shared": false, "is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z", "contacted_at": "2016-01-25T16:39:48.066Z",
"name": null, "name": null,
"online": true,
"status": "online",
"platform": null, "platform": null,
"projects": [ "projects": [
{ {
...@@ -327,14 +343,18 @@ Example response: ...@@ -327,14 +343,18 @@ Example response:
"description": "test-2-20150125", "description": "test-2-20150125",
"id": 8, "id": 8,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": false,
"status": "offline"
}, },
{ {
"active": true, "active": true,
"description": "development_runner", "description": "development_runner",
"id": 5, "id": 5,
"is_shared": true, "is_shared": true,
"name": null "name": null,
"online": true
"status": "paused"
} }
] ]
``` ```
...@@ -364,7 +384,9 @@ Example response: ...@@ -364,7 +384,9 @@ Example response:
"description": "test-2016-02-01", "description": "test-2016-02-01",
"id": 9, "id": 9,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": true,
"status": "online"
} }
``` ```
......
...@@ -70,9 +70,13 @@ PUT /application/settings ...@@ -70,9 +70,13 @@ PUT /application/settings
| `after_sign_up_text` | string | no | Text shown to the user after signing up | | `after_sign_up_text` | string | no | Text shown to the user after signing up |
| `akismet_api_key` | string | no | API key for akismet spam protection | | `akismet_api_key` | string | no | API key for akismet spam protection |
| `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | | `akismet_enabled` | boolean | no | Enable or disable akismet spam protection |
<<<<<<< HEAD
| `allow_group_owners_to_manage_ldap` | boolean | no | Set to `true` to allow group owners to manage LDAP | | `allow_group_owners_to_manage_ldap` | boolean | no | Set to `true` to allow group owners to manage LDAP |
| `authorized_keys_enabled` | boolean | no | By default, we write to the "authorized_keys" file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. | | `authorized_keys_enabled` | boolean | no | By default, we write to the "authorized_keys" file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. | | `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. |
=======
| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. |
>>>>>>> upstream/master
| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. | | `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. |
| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. | | `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. |
| `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | | `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. |
......
...@@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do ...@@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do
end end
``` ```
## Running tests with a locally modified version of Gitaly
Normally, gitlab-ce/ee tests use a local clone of Gitaly in `tmp/tests/gitaly`
pinned at the version specified in GITALY_SERVER_VERSION. If you want
to run tests locally against a modified version of Gitaly you can
replace `tmp/tests/gitaly` with a symlink.
```shell
rm -rf tmp/tests/gitaly
ln -s /path/to/gitaly tmp/tests/gitaly
```
Make sure you run `make` in your local Gitaly directory before running
tests. Otherwise, Gitaly will fail to boot.
If you make changes to your local Gitaly in between test runs you need
to manually run `make` again.
Note that CI tests will not use your locally modified version of
Gitaly. To use a custom Gitaly version in CI you need to update
GITALY_SERVER_VERSION. You can use the format `=revision` to use a
non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI.
--- ---
[Return to Development documentation](README.md) [Return to Development documentation](README.md)
# End-to-End Testing
## What is End-to-End testing?
End-to-End testing is a strategy used to check whether your application works
as expected across entire software stack and architecture, including
integration of all microservices and components that are supposed to work
together.
## How do we test GitLab?
We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we
test these packages using [GitLab QA][gitlab-qa] project, which is entirely
black-box, click-driven testing framework.
### Testing nightly builds
We run scheduled pipeline each night to test nightly builds created by Omnibus.
You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines].
### Testing code in merge requests
It is possible to run end-to-end tests (eventually being run within a
[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering
the `package-qa` manual action, that should be present in a merge request
widget.
Mmanual action that starts end-to-end tests is also available in merge requests
in Omnibus GitLab project.
Below you can read more about how to use it and how does it work.
#### How does it work?
Currently, we are using _multi-project pipeline_-like approach to run QA
pipelines.
1. Developer triggers a manual action, that can be found in CE and EE merge
requests. This starts a chain of pipelines in multiple projects.
1. The script being executed triggers a pipeline in GitLab Omnibus and waits
for the resulting status. We call this a _status attribution_.
1. GitLab packages are being built in Omnibus pipeline. Packages are going to be
pushed to Container Registry.
1. When packages are ready, and available in the registry, a final step in the
pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab
QA project. It also waits for a resulting status.
1. GitLab QA pulls images from the registry, spins-up containers and runs tests
against a test environment that has been just orchestrated by the `gitlab-qa`
tool.
1. The result of the GitLab QA pipeline is being propagated upstream, through
Omnibus, back to CE / EE merge request.
#### How do I write tests?
In order to write new tests, you first need to learn more about GitLab QA
architecture. See the [documentation about it][gitlab-qa-architecture] in
GitLab QA project.
Once you decided where to put test environment orchestration scenarios and
instance specs, take a look at the [relevant documentation][instance-qa-readme]
and examples in [the `qa/` directory][instance-qa-examples].
## Where can I ask for help?
You can ask question in the `#qa` channel on Slack (GitLab internal) or you can
find an issue you would like to work on in [the issue tracker][gitlab-qa-issues]
and start a new discussion there.
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md
[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues
[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md
[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa
...@@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks. ...@@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks.
--- ---
## [End-to-end tests](end_to_end_tests.md)
Everything you should know about how to run end-to-end tests using
[GitLab QA][gitlab-qa] testing framework.
---
## Spinach (feature) tests ## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
...@@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead. ...@@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead.
[Capybara]: https://github.com/teamcapybara/capybara [Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/ [Karma]: http://karma-runner.github.io/
[Jasmine]: https://jasmine.github.io/ [Jasmine]: https://jasmine.github.io/
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
...@@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it. ...@@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it.
The actual test scenarios and steps are [part of GitLab Rails] so that they're The actual test scenarios and steps are [part of GitLab Rails] so that they're
always in-sync with the codebase. always in-sync with the codebase.
Read a separate document about [end-to-end tests](end_to_end_tests.md) to
learn more.
[multiple pieces]: ../architecture.md#components [multiple pieces]: ../architecture.md#components
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell [GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse [GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
......
...@@ -894,6 +894,7 @@ module API ...@@ -894,6 +894,7 @@ module API
expose :id expose :id
expose :project, using: Entities::BasicProjectDetails expose :project, using: Entities::BasicProjectDetails
<<<<<<< HEAD
# EE-specific # EE-specific
# Default filtering configuration # Default filtering configuration
expose :name expose :name
...@@ -903,6 +904,8 @@ module API ...@@ -903,6 +904,8 @@ module API
expose :labels, using: Entities::LabelBasic, if: -> (board, _) { scoped_issue_available?(board) } expose :labels, using: Entities::LabelBasic, if: -> (board, _) { scoped_issue_available?(board) }
expose :weight, if: -> (board, _) { scoped_issue_available?(board) } expose :weight, if: -> (board, _) { scoped_issue_available?(board) }
=======
>>>>>>> upstream/master
expose :lists, using: Entities::List do |board| expose :lists, using: Entities::List do |board|
board.lists.destroyable board.lists.destroyable
end end
...@@ -997,6 +1000,8 @@ module API ...@@ -997,6 +1000,8 @@ module API
expose :active expose :active
expose :is_shared expose :is_shared
expose :name expose :name
expose :online?, as: :online
expose :status
end end
class RunnerDetails < Runner class RunnerDetails < Runner
...@@ -1290,6 +1295,7 @@ module API ...@@ -1290,6 +1295,7 @@ module API
class PagesDomainBasic < Grape::Entity class PagesDomainBasic < Grape::Entity
expose :domain expose :domain
expose :url expose :url
expose :project_id
expose :certificate, expose :certificate,
as: :certificate_expiration, as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? }, if: ->(pages_domain, _) { pages_domain.certificate? },
......
...@@ -283,6 +283,19 @@ module API ...@@ -283,6 +283,19 @@ module API
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end end
desc 'List participants for an issue' do
success Entities::UserBasic
end
params do
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
get ':id/issues/:issue_iid/participants' do
issue = find_project_issue(params[:issue_iid])
participants = ::Kaminari.paginate_array(issue.participants)
present paginate(participants), with: Entities::UserBasic, current_user: current_user, project: user_project
end
desc 'Get the user agent details for an issue' do desc 'Get the user agent details for an issue' do
success Entities::UserAgentDetail success Entities::UserAgentDetail
end end
......
...@@ -197,6 +197,16 @@ module API ...@@ -197,6 +197,16 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end end
desc 'Get the participants of a merge request' do
success Entities::UserBasic
end
get ':id/merge_requests/:merge_request_iid/participants' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
participants = ::Kaminari.paginate_array(merge_request.participants)
present paginate(participants), with: Entities::UserBasic
end
desc 'Get the commits of a merge request' do desc 'Get the commits of a merge request' do
success Entities::Commit success Entities::Commit
end end
......
...@@ -2,16 +2,7 @@ module Banzai ...@@ -2,16 +2,7 @@ module Banzai
module Filter module Filter
class MermaidFilter < HTML::Pipeline::Filter class MermaidFilter < HTML::Pipeline::Filter
def call def call
doc.css('pre[lang="mermaid"]').add_class('mermaid') doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid')
doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid')
# The `<code></code>` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb
# We want to keep context and consistency, so we the blocks are added for all filters.
# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859
doc.css('pre[lang="mermaid"]').each do |pre|
document = pre.at('code')
document.replace(document.content)
end
doc doc
end end
......
...@@ -14,14 +14,7 @@ module Gitlab ...@@ -14,14 +14,7 @@ module Gitlab
ENCODING_CONFIDENCE_THRESHOLD = 50 ENCODING_CONFIDENCE_THRESHOLD = 50
def encode!(message) def encode!(message)
return nil unless message.respond_to?(:force_encoding) message = force_encode_utf8(message)
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
if message.respond_to?(:frozen?) && message.frozen?
message = message.dup
end
message.force_encoding("UTF-8")
return message if message.valid_encoding? return message if message.valid_encoding?
# return message if message type is binary # return message if message type is binary
...@@ -35,6 +28,8 @@ module Gitlab ...@@ -35,6 +28,8 @@ module Gitlab
# encode and clean the bad chars # encode and clean the bad chars
message.replace clean(message) message.replace clean(message)
rescue ArgumentError
return nil
rescue rescue
encoding = detect ? detect[:encoding] : "unknown" encoding = detect ? detect[:encoding] : "unknown"
"--broken encoding: #{encoding}" "--broken encoding: #{encoding}"
...@@ -54,8 +49,8 @@ module Gitlab ...@@ -54,8 +49,8 @@ module Gitlab
end end
def encode_utf8(message) def encode_utf8(message)
return nil unless message.is_a?(String) message = force_encode_utf8(message)
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding? return message if message.valid_encoding?
detect = CharlockHolmes::EncodingDetector.detect(message) detect = CharlockHolmes::EncodingDetector.detect(message)
if detect && detect[:encoding] if detect && detect[:encoding]
...@@ -69,6 +64,8 @@ module Gitlab ...@@ -69,6 +64,8 @@ module Gitlab
else else
clean(message) clean(message)
end end
rescue ArgumentError
return nil
end end
def encode_binary(s) def encode_binary(s)
...@@ -83,6 +80,15 @@ module Gitlab ...@@ -83,6 +80,15 @@ module Gitlab
private private
def force_encode_utf8(message)
raise ArgumentError unless message.respond_to?(:force_encoding)
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
message = message.dup if message.respond_to?(:frozen?) && message.frozen?
message.force_encoding("UTF-8")
end
def clean(message) def clean(message)
message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "") message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
.encode("UTF-8") .encode("UTF-8")
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
def ref_name(ref) def ref_name(ref)
encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
end end
def branch_name(ref) def branch_name(ref)
......
...@@ -50,10 +50,19 @@ module Gitlab ...@@ -50,10 +50,19 @@ module Gitlab
# to the caller to limit the number of blobs and blob_size_limit. # to the caller to limit the number of blobs and blob_size_limit.
# #
# Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798 # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798
def batch(repository, blob_references, blob_size_limit: nil) def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
blob_size_limit ||= MAX_DATA_DISPLAY_SIZE Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled|
blob_references.map do |sha, path| if is_enabled
find_by_rugged(repository, sha, path, limit: blob_size_limit) Gitlab::GitalyClient.allow_n_plus_1_calls do
blob_references.map do |sha, path|
find_by_gitaly(repository, sha, path, limit: blob_size_limit)
end
end
else
blob_references.map do |sha, path|
find_by_rugged(repository, sha, path, limit: blob_size_limit)
end
end
end end
end end
...@@ -122,13 +131,23 @@ module Gitlab ...@@ -122,13 +131,23 @@ module Gitlab
) )
end end
def find_by_gitaly(repository, sha, path) def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
path = path.sub(/\A\/*/, '') path = path.sub(/\A\/*/, '')
path = '/' if path.empty? path = '/' if path.empty?
name = File.basename(path) name = File.basename(path)
entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE)
# Gitaly will think that setting the limit to 0 means unlimited, while
# the client might only need the metadata and thus set the limit to 0.
# In this method we'll then set the limit to 1, but clear the byte of data
# that we got back so for the outside world it looks like the limit was
# actually 0.
req_limit = limit == 0 ? 1 : limit
entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
return unless entry return unless entry
entry.data = "" if limit == 0
case entry.type case entry.type
when :COMMIT when :COMMIT
new( new(
......
...@@ -498,11 +498,13 @@ module Gitlab ...@@ -498,11 +498,13 @@ module Gitlab
end end
def count_commits(options) def count_commits(options)
count_commits_options = process_count_commits_options(options)
gitaly_migrate(:count_commits) do |is_enabled| gitaly_migrate(:count_commits) do |is_enabled|
if is_enabled if is_enabled
count_commits_by_gitaly(options) count_commits_by_gitaly(count_commits_options)
else else
count_commits_by_shelling_out(options) count_commits_by_shelling_out(count_commits_options)
end end
end end
end end
...@@ -540,8 +542,8 @@ module Gitlab ...@@ -540,8 +542,8 @@ module Gitlab
end end
# Counts the amount of commits between `from` and `to`. # Counts the amount of commits between `from` and `to`.
def count_commits_between(from, to) def count_commits_between(from, to, options = {})
count_commits(ref: "#{from}..#{to}") count_commits(from: from, to: to, **options)
end end
# Returns the SHA of the most recent common ancestor of +from+ and +to+ # Returns the SHA of the most recent common ancestor of +from+ and +to+
...@@ -1462,6 +1464,26 @@ module Gitlab ...@@ -1462,6 +1464,26 @@ module Gitlab
end end
end end
def process_count_commits_options(options)
if options[:from] || options[:to]
ref =
if options[:left_right] # Compare with merge-base for left-right
"#{options[:from]}...#{options[:to]}"
else
"#{options[:from]}..#{options[:to]}"
end
options.merge(ref: ref)
elsif options[:ref] && options[:left_right]
from, to = options[:ref].match(/\A([^\.]*)\.{2,3}([^\.]*)\z/)[1..2]
options.merge(from: from, to: to)
else
options
end
end
def log_using_shell?(options) def log_using_shell?(options)
options[:path].present? || options[:path].present? ||
options[:disable_walk] || options[:disable_walk] ||
...@@ -1684,20 +1706,59 @@ module Gitlab ...@@ -1684,20 +1706,59 @@ module Gitlab
end end
def count_commits_by_gitaly(options) def count_commits_by_gitaly(options)
gitaly_commit_client.commit_count(options[:ref], options) if options[:left_right]
from = options[:from]
to = options[:to]
right_count = gitaly_commit_client
.commit_count("#{from}..#{to}", options)
left_count = gitaly_commit_client
.commit_count("#{to}..#{from}", options)
[left_count, right_count]
else
gitaly_commit_client.commit_count(options[:ref], options)
end
end end
def count_commits_by_shelling_out(options) def count_commits_by_shelling_out(options)
cmd = count_commits_shelling_command(options)
raw_output = IO.popen(cmd) { |io| io.read }
process_count_commits_raw_output(raw_output, options)
end
def count_commits_shelling_command(options)
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
cmd << "--after=#{options[:after].iso8601}" if options[:after] cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count] cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
cmd << "--left-right" if options[:left_right]
cmd += %W[--count #{options[:ref]}] cmd += %W[--count #{options[:ref]}]
cmd += %W[-- #{options[:path]}] if options[:path].present? cmd += %W[-- #{options[:path]}] if options[:path].present?
cmd
end
raw_output = IO.popen(cmd) { |io| io.read } def process_count_commits_raw_output(raw_output, options)
if options[:left_right]
result = raw_output.scan(/\d+/).map(&:to_i)
if result.sum != options[:max_count]
result
else # Reaching max count, right is not accurate
right_option =
process_count_commits_options(options
.except(:left_right, :from, :to)
.merge(ref: options[:to]))
right = count_commits_by_shelling_out(right_option)
raw_output.to_i [result.first, right] # left should be accurate in the first call
end
else
raw_output.to_i
end
end end
def gitaly_ls_files(ref) def gitaly_ls_files(ref)
......
...@@ -15,6 +15,11 @@ module Gitlab ...@@ -15,6 +15,11 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end end
def git_clone_bundle(repo_path:, bundle_path:)
execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path}))
Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path))
end
def mkdir_p(path) def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path) FileUtils.chmod(DEFAULT_MODE, path)
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
def restore def restore
return true unless File.exist?(@path_to_bundle) return true unless File.exist?(@path_to_bundle)
gitlab_shell.import_repository(@project.repository_storage_path, @project.disk_path, @path_to_bundle) git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle)
rescue => e rescue => e
@shared.error(e) @shared.error(e)
false false
......
module Gitlab
module SetupHelper
class << self
# We cannot create config.toml files for all possible Gitaly configuations.
# For instance, if Gitaly is running on another machine then it makes no
# sense to write a config.toml file on the current machine. This method will
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
# For development and testing purposes, an extra storage is added to gitaly,
# which is not known to Rails, but must be explicitly stubbed.
def gitaly_configuration_toml(gitaly_dir, gitaly_ruby: true)
storages = []
address = nil
Gitlab.config.repositories.storages.each do |key, val|
if address
if address != val['gitaly_address']
raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
end
elsif URI(val['gitaly_address']).scheme != 'unix'
raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
else
address = val['gitaly_address']
end
storages << { name: key, path: val['path'] }
end
if Rails.env.test?
storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
end
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
TOML.dump(config)
end
# rubocop:disable Rails/Output
def create_gitaly_configuration(dir, force: false)
config_path = File.join(dir, 'config.toml')
FileUtils.rm_f(config_path) if force
File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f|
f.puts gitaly_configuration_toml(dir)
end
rescue Errno::EEXIST
puts "Skipping config.toml generation:"
puts "A configuration file already exists."
rescue ArgumentError => e
puts "Skipping config.toml generation:"
puts e.message
end
# rubocop:enable Rails/Output
end
end
end
...@@ -71,7 +71,6 @@ module Gitlab ...@@ -71,7 +71,6 @@ module Gitlab
# Ex. # Ex.
# add_repository("/path/to/storage", "gitlab/gitlab-ci") # add_repository("/path/to/storage", "gitlab/gitlab-ci")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def add_repository(storage, name) def add_repository(storage, name)
relative_path = name.dup relative_path = name.dup
relative_path << '.git' unless relative_path.end_with?('.git') relative_path << '.git' unless relative_path.end_with?('.git')
...@@ -100,8 +99,12 @@ module Gitlab ...@@ -100,8 +99,12 @@ module Gitlab
# Ex. # Ex.
# import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874
def import_repository(storage, name, url) def import_repository(storage, name, url)
if url.start_with?('.', '/')
raise Error.new("don't use disk paths with import_repository: #{url.inspect}")
end
# The timeout ensures the subprocess won't hang forever # The timeout ensures the subprocess won't hang forever
cmd = gitlab_projects(storage, "#{name}.git") cmd = gitlab_projects(storage, "#{name}.git")
success = cmd.import_project(url, git_timeout) success = cmd.import_project(url, git_timeout)
...@@ -122,7 +125,6 @@ module Gitlab ...@@ -122,7 +125,6 @@ module Gitlab
# Ex. # Ex.
# fetch_remote(my_repo, "upstream") # fetch_remote(my_repo, "upstream")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false) def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false)
gitaly_migrate(:fetch_remote) do |is_enabled| gitaly_migrate(:fetch_remote) do |is_enabled|
if is_enabled if is_enabled
...@@ -142,7 +144,7 @@ module Gitlab ...@@ -142,7 +144,7 @@ module Gitlab
# Ex. # Ex.
# mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new") # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def mv_repository(storage, path, new_path) def mv_repository(storage, path, new_path)
gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git") gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git")
end end
...@@ -156,7 +158,7 @@ module Gitlab ...@@ -156,7 +158,7 @@ module Gitlab
# Ex. # Ex.
# fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "new-namespace/gitlab-ci") # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "new-namespace/gitlab-ci")
# #
# Gitaly note: JV: not easy to migrate because this involves two Gitaly servers, not one. # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817
def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path) def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path)
gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git") gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git")
.fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git")
...@@ -170,7 +172,7 @@ module Gitlab ...@@ -170,7 +172,7 @@ module Gitlab
# Ex. # Ex.
# remove_repository("/path/to/storage", "gitlab/gitlab-ci") # remove_repository("/path/to/storage", "gitlab/gitlab-ci")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873
def remove_repository(storage, name) def remove_repository(storage, name)
gitlab_projects(storage, "#{name}.git").rm_project gitlab_projects(storage, "#{name}.git").rm_project
end end
...@@ -279,7 +281,6 @@ module Gitlab ...@@ -279,7 +281,6 @@ module Gitlab
# Ex. # Ex.
# add_namespace("/path/to/storage", "gitlab") # add_namespace("/path/to/storage", "gitlab")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def add_namespace(storage, name) def add_namespace(storage, name)
Gitlab::GitalyClient.migrate(:add_namespace) do |enabled| Gitlab::GitalyClient.migrate(:add_namespace) do |enabled|
if enabled if enabled
...@@ -301,7 +302,6 @@ module Gitlab ...@@ -301,7 +302,6 @@ module Gitlab
# Ex. # Ex.
# rm_namespace("/path/to/storage", "gitlab") # rm_namespace("/path/to/storage", "gitlab")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def rm_namespace(storage, name) def rm_namespace(storage, name)
Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled| Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled|
if enabled if enabled
...@@ -319,7 +319,6 @@ module Gitlab ...@@ -319,7 +319,6 @@ module Gitlab
# Ex. # Ex.
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq") # mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def mv_namespace(storage, old_name, new_name) def mv_namespace(storage, old_name, new_name)
Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled| Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled|
if enabled if enabled
......
...@@ -390,14 +390,8 @@ namespace :gitlab do ...@@ -390,14 +390,8 @@ namespace :gitlab do
namespace :repo do namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab" desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do task check: :environment do
Gitlab.config.repositories.storages.each do |name, repository_storage| puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red)
namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*')) Rake::Task["gitlab:git:fsck"].execute
namespace_dirs.each do |namespace_dir|
repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
end
end
end end
end end
...@@ -491,35 +485,4 @@ namespace :gitlab do ...@@ -491,35 +485,4 @@ namespace :gitlab do
puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red) puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red)
end end
end end
def check_repo_integrity(repo_dir)
puts "\nChecking repo at #{repo_dir.color(:yellow)}"
git_fsck(repo_dir)
check_config_lock(repo_dir)
check_ref_locks(repo_dir)
end
def git_fsck(repo_dir)
puts "Running `git fsck`".color(:yellow)
system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir)
end
def check_config_lock(repo_dir)
config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end
def check_ref_locks(repo_dir)
lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
if lock_files.present?
puts "Ref lock files exist:".color(:red)
lock_files.each do |lock_file|
puts " #{lock_file}"
end
else
puts "No ref lock files exist".color(:green)
end
end
end end
...@@ -30,6 +30,20 @@ namespace :gitlab do ...@@ -30,6 +30,20 @@ namespace :gitlab do
end end
end end
desc 'GitLab | Git | Check all repos integrity'
task fsck: :environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo|
check_config_lock(repo)
check_ref_locks(repo)
end
if failures.empty?
puts "Done".color(:green)
else
output_failures(failures)
end
end
def perform_git_cmd(cmd, message) def perform_git_cmd(cmd, message)
puts "Starting #{message} on all repositories" puts "Starting #{message} on all repositories"
...@@ -40,6 +54,8 @@ namespace :gitlab do ...@@ -40,6 +54,8 @@ namespace :gitlab do
else else
failures << repo failures << repo
end end
yield(repo) if block_given?
end end
failures failures
...@@ -49,5 +65,24 @@ namespace :gitlab do ...@@ -49,5 +65,24 @@ namespace :gitlab do
puts "The following repositories reported errors:".color(:red) puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" } failures.each { |f| puts "- #{f}" }
end end
def check_config_lock(repo_dir)
config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end
def check_ref_locks(repo_dir)
lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
if lock_files.present?
puts "Ref lock files exist:".color(:red)
lock_files.each { |lock_file| puts " #{lock_file}" }
else
puts "No ref lock files exist".color(:green)
end
end
end end
end end
...@@ -21,8 +21,8 @@ namespace :gitlab do ...@@ -21,8 +21,8 @@ namespace :gitlab do
command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test?
Gitlab::SetupHelper.create_gitaly_configuration(args.dir)
Dir.chdir(args.dir) do Dir.chdir(args.dir) do
create_gitaly_configuration
# In CI we run scripts/gitaly-test-build instead of this command # In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present? unless ENV['CI'].present?
Bundler.with_original_env { run_command!(command) } Bundler.with_original_env { run_command!(command) }
...@@ -39,60 +39,7 @@ namespace :gitlab do ...@@ -39,60 +39,7 @@ namespace :gitlab do
# Exclude gitaly-ruby configuration because that depends on the gitaly # Exclude gitaly-ruby configuration because that depends on the gitaly
# installation directory. # installation directory.
puts gitaly_configuration_toml(gitaly_ruby: false) puts Gitlab::SetupHelper.gitaly_configuration_toml('', gitaly_ruby: false)
end
private
# We cannot create config.toml files for all possible Gitaly configuations.
# For instance, if Gitaly is running on another machine then it makes no
# sense to write a config.toml file on the current machine. This method will
# only generate a configuration for the most common and simplest case: when
# we have exactly one Gitaly process and we are sure it is running locally
# because it uses a Unix socket.
# For development and testing purposes, an extra storage is added to gitaly,
# which is not known to Rails, but must be explicitly stubbed.
def gitaly_configuration_toml(gitaly_ruby: true)
storages = []
address = nil
Gitlab.config.repositories.storages.each do |key, val|
if address
if address != val['gitaly_address']
raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
end
elsif URI(val['gitaly_address']).scheme != 'unix'
raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
else
address = val['gitaly_address']
end
storages << { name: key, path: val['path'] }
end
if Rails.env.test?
storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
end
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
TOML.dump(config)
end
def create_gitaly_configuration
File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f|
f.puts gitaly_configuration_toml
end
rescue Errno::EEXIST
puts "Skipping config.toml generation:"
puts "A configuration file already exists."
rescue ArgumentError => e
puts "Skipping config.toml generation:"
puts e.message
end end
end end
end end
...@@ -130,7 +130,7 @@ module Gitlab ...@@ -130,7 +130,7 @@ module Gitlab
def all_repos def all_repos
Gitlab.config.repositories.storages.each_value do |repository_storage| Gitlab.config.repositories.storages.each_value do |repository_storage|
IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -type d -name *.git)) do |find|
find.each_line do |path| find.each_line do |path|
yield path.chomp yield path.chomp
end end
......
...@@ -284,6 +284,102 @@ describe 'Copy as GFM', :js do ...@@ -284,6 +284,102 @@ describe 'Copy as GFM', :js do
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
verify(
'MermaidFilter: mermaid as converted from GFM to HTML',
<<-GFM.strip_heredoc
```mermaid
graph TD;
A-->B;
```
GFM
)
aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do
gfm = <<-GFM.strip_heredoc
```mermaid
graph TD;
A-->B;
```
GFM
html = <<-HTML.strip_heredoc
<svg id="mermaidChart1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 87.234375 174" style="max-width:87.234375px;" class="mermaid">
<style>
.mermaid {
/* Flowchart variables */
/* Sequence Diagram variables */
/* Gantt chart variables */
/** Section styling */
/* Grid and axis */
/* Today line */
/* Task styling */
/* Default task */
/* Specific task settings for the sections*/
/* Active task */
/* Completed task */
/* Tasks on the critical line */
}
</style>
<g>
<g class="output">
<g class="clusters"></g>
<g class="edgePaths">
<g class="edgePath" style="opacity: 1;">
<path class="path" d="M33.6171875,52L33.6171875,77L33.6171875,102" marker-end="url(#arrowhead65)" style="fill:none"></path>
<defs>
<marker id="arrowhead65" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path>
</marker>
</defs>
</g>
</g>
<g class="edgeLabels">
<g class="edgeLabel" style="opacity: 1;" transform="">
<g transform="translate(0,0)" class="label">
<foreignObject width="0" height="0">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">
<span class="edgeLabel"></span>
</div>
</foreignObject>
</g>
</g>
</g>
<g class="nodes">
<g class="node" id="A" transform="translate(33.6171875,36)" style="opacity: 1;">
<rect rx="0" ry="0" x="-13.6171875" y="-16" width="27.234375" height="32"></rect>
<g class="label" transform="translate(0,0)">
<g transform="translate(-3.6171875,-6)">
<foreignObject width="7.234375" height="12">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">A</div>
</foreignObject>
</g>
</g>
</g>
<g class="node" id="B" transform="translate(33.6171875,118)" style="opacity: 1;">
<rect rx="0" ry="0" x="-13.6171875" y="-16" width="27.234375" height="32">
</rect>
<g class="label" transform="translate(0,0)">
<g transform="translate(-3.6171875,-6)">
<foreignObject width="7.234375" height="12">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">B</div>
</foreignObject>
</g>
</g>
</g>
</g>
</g>
</g>
<text class="source" display="none">graph TD;
A--&gt;B;
</text>
</svg>
HTML
output_gfm = html_to_gfm(html)
expect(output_gfm.strip).to eq(gfm.strip)
end
verify( verify(
'SanitizationFilter', 'SanitizationFilter',
......
...@@ -80,7 +80,7 @@ describe 'GitLab Markdown' do ...@@ -80,7 +80,7 @@ describe 'GitLab Markdown' do
it 'parses mermaid code block' do it 'parses mermaid code block' do
aggregate_failures do aggregate_failures do
expect(doc).to have_selector('pre.code.js-render-mermaid') expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid')
end end
end end
......
...@@ -81,14 +81,14 @@ feature 'Gcp Cluster', :js do ...@@ -81,14 +81,14 @@ feature 'Gcp Cluster', :js do
end end
it 'user sees a cluster details page' do it 'user sees a cluster details page' do
expect(page).to have_button('Save') expect(page).to have_button('Save changes')
expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end end
context 'when user disables the cluster' do context 'when user disables the cluster' do
before do before do
page.find(:css, '.js-toggle-cluster').click page.find(:css, '.js-toggle-cluster').click
click_button 'Save' page.within('#cluster-integration') { click_button 'Save changes' }
end end
it 'user sees the successful message' do it 'user sees the successful message' do
...@@ -99,7 +99,7 @@ feature 'Gcp Cluster', :js do ...@@ -99,7 +99,7 @@ feature 'Gcp Cluster', :js do
context 'when user changes cluster parameters' do context 'when user changes cluster parameters' do
before do before do
fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
click_button 'Save changes' page.within('#js-cluster-details') { click_button 'Save changes' }
end end
it 'user sees the successful message' do it 'user sees the successful message' do
......
...@@ -29,7 +29,7 @@ feature 'User Cluster', :js do ...@@ -29,7 +29,7 @@ feature 'User Cluster', :js do
end end
it 'user sees a cluster details page' do it 'user sees a cluster details page' do
expect(page).to have_content('Enable cluster integration') expect(page).to have_content('Cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster') expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com') .to have_content('http://example.com')
...@@ -57,14 +57,14 @@ feature 'User Cluster', :js do ...@@ -57,14 +57,14 @@ feature 'User Cluster', :js do
end end
it 'user sees a cluster details page' do it 'user sees a cluster details page' do
expect(page).to have_button('Save') expect(page).to have_button('Save changes')
end end
context 'when user disables the cluster' do context 'when user disables the cluster' do
before do before do
page.find(:css, '.js-toggle-cluster').click page.find(:css, '.js-toggle-cluster').click
fill_in 'cluster_name', with: 'dev-cluster' fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Save' page.within('#cluster-integration') { click_button 'Save changes' }
end end
it 'user sees the successful message' do it 'user sees the successful message' do
...@@ -76,7 +76,7 @@ feature 'User Cluster', :js do ...@@ -76,7 +76,7 @@ feature 'User Cluster', :js do
before do before do
fill_in 'cluster_name', with: 'my-dev-cluster' fill_in 'cluster_name', with: 'my-dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
click_button 'Save changes' page.within('#js-cluster-details') { click_button 'Save changes' }
end end
it 'user sees the successful message' do it 'user sees the successful message' do
......
...@@ -108,6 +108,7 @@ ...@@ -108,6 +108,7 @@
"rebase_commit_sha": { "type": ["string", "null"] }, "rebase_commit_sha": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" }, "rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" }, "can_push_to_source_branch": { "type": "boolean" },
<<<<<<< HEAD
"rebase_path": { "type": ["string", "null"] }, "rebase_path": { "type": ["string", "null"] },
// EE-specific // EE-specific
...@@ -121,6 +122,9 @@ ...@@ -121,6 +122,9 @@
"base_path": { "type": "string" }, "base_path": { "type": "string" },
"base_blob_path": { "type": "string" } "base_blob_path": { "type": "string" }
} }
=======
"rebase_path": { "type": ["string", "null"] }
>>>>>>> upstream/master
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
"properties": { "properties": {
"domain": { "type": "string" }, "domain": { "type": "string" },
"url": { "type": "uri" }, "url": { "type": "uri" },
"project_id": { "type": "integer" },
"certificate_expiration": { "certificate_expiration": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -13,6 +14,6 @@ ...@@ -13,6 +14,6 @@
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": ["domain", "url"], "required": ["domain", "url", "project_id"],
"additionalProperties": false "additionalProperties": false
} }
...@@ -47,17 +47,11 @@ describe('ItemActionsComponent', () => { ...@@ -47,17 +47,11 @@ describe('ItemActionsComponent', () => {
it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => {
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
vm.modalStatus = true; vm.modalStatus = true;
vm.leaveGroup(true);
expect(vm.modalStatus).toBeFalsy();
expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup);
});
it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { vm.leaveGroup();
spyOn(eventHub, '$emit');
vm.modalStatus = true;
vm.leaveGroup(false);
expect(vm.modalStatus).toBeFalsy(); expect(vm.modalStatus).toBeFalsy();
expect(eventHub.$emit).not.toHaveBeenCalled(); expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup);
}); });
}); });
}); });
......
...@@ -55,12 +55,15 @@ describe('text_utility', () => { ...@@ -55,12 +55,15 @@ describe('text_utility', () => {
}); });
}); });
<<<<<<< HEAD
describe('capitalizeFirstCharacter', () => { describe('capitalizeFirstCharacter', () => {
it('returns string with first letter capitalized', () => { it('returns string with first letter capitalized', () => {
expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab'); expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
}); });
}); });
=======
>>>>>>> upstream/master
describe('stripeHtml', () => { describe('stripeHtml', () => {
it('replaces html tag with the default replacement', () => { it('replaces html tag with the default replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
......
...@@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => { ...@@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
expect(vm.enteredPassword).toBe(input.value); expect(vm.enteredPassword).toBe(input.value);
expect(submitButton).toHaveClass('disabled'); expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click(); submitButton.click();
expect(form.submit).not.toHaveBeenCalled(); expect(form.submit).not.toHaveBeenCalled();
}) })
...@@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => { ...@@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
expect(vm.enteredPassword).toBe(input.value); expect(vm.enteredPassword).toBe(input.value);
expect(submitButton).not.toHaveClass('disabled'); expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click(); submitButton.click();
expect(form.submit).toHaveBeenCalled(); expect(form.submit).toHaveBeenCalled();
}) })
...@@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => { ...@@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
expect(vm.enteredUsername).toBe(input.value); expect(vm.enteredUsername).toBe(input.value);
expect(submitButton).toHaveClass('disabled'); expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click(); submitButton.click();
expect(form.submit).not.toHaveBeenCalled(); expect(form.submit).not.toHaveBeenCalled();
}) })
...@@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => { ...@@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
expect(vm.enteredUsername).toBe(input.value); expect(vm.enteredUsername).toBe(input.value);
expect(submitButton).not.toHaveClass('disabled'); expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click(); submitButton.click();
expect(form.submit).toHaveBeenCalled(); expect(form.submit).toHaveBeenCalled();
}) })
......
...@@ -41,11 +41,11 @@ describe('IdeRepoTree', () => { ...@@ -41,11 +41,11 @@ describe('IdeRepoTree', () => {
expect(tbody.querySelector('.file')).toBeTruthy(); expect(tbody.querySelector('.file')).toBeTruthy();
}); });
it('renders 5 loading files if tree is loading', (done) => { it('renders 3 loading files if tree is loading', (done) => {
vm.$store.state.loading = true; vm.treeId = '123';
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3);
done(); done();
}); });
......
...@@ -10,7 +10,9 @@ describe('ide component', () => { ...@@ -10,7 +10,9 @@ describe('ide component', () => {
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(ide); const Component = Vue.extend(ide);
vm = createComponentWithStore(Component, store).$mount(); vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
}).$mount();
}); });
afterEach(() => { afterEach(() => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment