Commit cd610b69 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee-2017-11-15

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 77d4e6ae 4be68c4f
...@@ -603,7 +603,7 @@ codequality: ...@@ -603,7 +603,7 @@ codequality:
script: script:
- cp .rubocop.yml .rubocop.yml.bak - cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml - mv .rubocop.yml.bak .rubocop.yml
artifacts: artifacts:
......
{"iconCount":173,"spriteSize":75815,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","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","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","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","pipeline","play","plus-square-o","plus-square","plus","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":174,"spriteSize":76324,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","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","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","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","pipeline","play","plus-square-o","plus-square","plus","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"]}
\ No newline at end of file \ No newline at end of file
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" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="b" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="c" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="d" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="e" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="f" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="rotate(5 202.071 210.085)" rx="10"/><g transform="rotate(15 -104.714 891.23)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#d2caea" fill-rule="nonzero" d="M96.153 81.151a2.001 2.001 0 0 0 2.184-.496l35.956-38.34a2 2 0 1 0-2.918-2.736l-35.03 37.36-41.888-16.285a2 2 0 0 0-2.16.471l-26.368 27.16a2 2 0 1 0 2.87 2.786l25.444-26.21 41.911 16.294"/><g fill="#fff" transform="translate(24.368 36.951)"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="rotate(-5 116.372 150.825)" rx="10"/><g transform="rotate(5 -1514.687 1518.752)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#e)" xlink:href="#b"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="M84.67 28.41c18.225 0 33 15.07 33 33.651h-33V28.41" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="M78.67 66.41h30a2 2 0 0 1 2 2c0 18.778-15.222 34-34 34s-34-15.222-34-34 15.222-34 34-34a2 2 0 0 1 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28H76.67a2 2 0 0 1-2-2V38.476c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="rotate(-5 1023.06 -299.524)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#f)" xlink:href="#c"/><path fill="#fef0ea" d="M42 47.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391V97H42V47.391"/><path fill="#fb722e" d="M108 55.406c0-.777.628-1.406 1.4-1.406h9.2a1.4 1.4 0 0 1 1.4 1.406V97h-12V55.406"/><path fill="#6b4fbb" d="M64 35.404c0-.776.628-1.404 1.4-1.404h9.2a1.4 1.4 0 0 1 1.4 1.404v61.6H64v-61.6"/><path fill="#d2caea" d="M86 73.4a1.4 1.4 0 0 1 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602H86V73.4"/></g><g fill="#fee8dc"><path d="M3.592 93.86l-2.454-1.562c-.93-.592-.924-1.554 0-2.143l2.454-1.562 1.562-2.454c.592-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143L8.86 93.86l-1.562 2.454c-.591.93-1.554.924-2.143 0L3.592 93.86M309.489 52.07l-3.14-1.998c-1.12-.713-1.128-1.863 0-2.581l3.14-2 1.999-3.14c.713-1.12 1.863-1.127 2.58 0l2 3.14 3.14 2c1.12.713 1.128 1.863 0 2.58l-3.14 2-2 3.14c-.712 1.12-1.862 1.128-2.58 0l-1.999-3.14"/></g><path fill="#e1dcf1" d="M128.073 11.066l-1.99 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/><path fill="#d2caea" d="M378.07 243.068l-1.989 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/></g></svg>
\ No newline at end of file \ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M59.65 32.65H60l-2-2.42-2 2.4-2-2.4-2 2.4-2-2.4-2 2.4-2-2.4-2 2.42h.77C45.57 34.6 46 36.75 46 39c0 2.84-.7 5.5-1.92 7.86 1.97 2.28 4.83 3.64 7.92 3.64 5.8 0 10.5-4.74 10.5-10.6 0-2.8-1.08-5.36-2.85-7.25zM43.18 29.6c2.4-2.1 5.52-3.3 8.82-3.3 7.46 0 13.5 6.1 13.5 13.6S59.46 53.5 52 53.5c-3.68 0-7.1-1.5-9.6-4.04C39.3 53.44 34.44 56 29 56c-9.4 0-17-7.6-17-17s7.6-17 17-17c3.22 0 6.23.9 8.8 2.45 2.13 1.3 3.97 3.05 5.38 5.16zM17 34c-.65 1.54-1 3.23-1 5 0 7.18 5.82 13 13 13s13-5.82 13-13c0-1.77-.35-3.46-1-5h-9c-.53 0-1.04-.2-1.4-.6L29 31.84l-1.6 1.58c-.36.4-.87.6-1.4.6h-9zm21.38-4a12.996 12.996 0 0 0-18.76 0h5.55l2.42-2.4c.74-.8 2-.8 2.8 0l2.4 2.4h5.54z"/><path fill="#6B4FBB" d="M47.6 42.32c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zm8.8 0c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zM25 44h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-1c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#6B4FBB" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z"/><path fill="#6B4FBB" d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M24.92 35.15a4.012 4.012 0 0 1-.6-5.63l1.26-1.55c1.4-1.72 3.9-2 5.63-.6l.7.56c.7-.4 1.4-.73 2.1-1V26c0-2.2 1.8-4 4-4h2c2.2 0 4 1.8 4 4v.92c.8.28 1.5.62 2.1 1l.7-.55c1.7-1.4 4.3-1.12 5.7.6l1.3 1.55c1.4 1.72 1.2 4.23-.6 5.63l-.7.6c.3.74.4 1.5.5 2.3l.9.2c2.2.5 3.5 2.64 3 4.8L56.4 45c-.5 2.15-2.64 3.5-4.8 3l-.88-.2c-.44.63-.92 1.24-1.46 1.8l.4.82c.9 1.98.1 4.38-1.9 5.35l-1.8.87c-2 .97-4.37.15-5.34-1.84l-.46-.85c-.34.03-.74.05-1.13.05-.4 0-.8-.02-1.2-.05l-.4.85c-.95 2-3.34 2.8-5.33 1.84l-1.8-.87a4.011 4.011 0 0 1-1.83-5.35l.4-.8c-.54-.58-1.02-1.2-1.46-1.83l-.8.2c-2.2.5-4.3-.9-4.8-3l-.4-2c-.5-2.2.85-4.3 3-4.8l.9-.2c.1-.8.3-1.6.5-2.3l-.7-.6zm4.95.77c-.53 1.2-.83 2.47-.87 3.8-.02.9-.66 1.68-1.55 1.9l-2.32.53.45 1.94 2.3-.6c.9-.2 1.8.2 2.23 1 .7 1.1 1.5 2.2 2.5 3 .7.6.9 1.6.5 2.4l-1 2.1 1.8.9 1.1-2.1c.4-.8 1.3-1.3 2.2-1.1.7.1 1.3.2 2 .2s1.3-.1 2-.2c.9-.2 1.8.3 2.2 1.1l1 2.1 1.8-.9-1.2-2c-.4-.8-.2-1.8.5-2.4 1-.85 1.84-1.88 2.45-3.05.4-.82 1.33-1.24 2.2-1.04l2.33.54.45-1.95-2.32-.54c-.9-.2-1.52-.97-1.54-1.88-.03-1.4-.33-2.6-.86-3.8-.4-.9-.2-1.8.5-2.4l1.9-1.5-1.3-1.6-1.8 1.5c-.8.5-1.8.6-2.5 0-1.1-.8-2.3-1.4-3.5-1.7-.9-.2-1.5-1-1.5-1.9V26h-2v2.38c0 .9-.6 1.7-1.5 1.93-1.3.4-2.5 1-3.5 1.7-.8.6-1.8.6-2.5 0l-1.9-1.5-1.26 1.6 1.8 1.5c.7.6.94 1.6.6 2.4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M39 46c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="330" height="132" viewBox="0 0 330 132"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M174.12 42c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M211 78c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S230.33 4 211 4s-35 15.67-35 35 15.67 35 35 35z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M211.5 51c-6.42 0-12.26-2.84-17.43-8.4a4.008 4.008 0 0 1-.27-5.13C199 30.57 204.92 27 211.5 27s12.5 3.56 17.7 10.47a3.994 3.994 0 0 1-.27 5.12c-5.17 5.53-11 8.4-17.43 8.4zm0-4c5.25 0 10.05-2.34 14.5-7.13-4.5-5.98-9.3-8.87-14.5-8.87-5.2 0-10 2.9-14.5 8.87 4.45 4.8 9.25 7.13 14.5 7.13z"/><path fill="#FC6D26" d="M211 47c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-4c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0-1c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></g><path fill="#000" fill-opacity=".03" d="M88.12 83c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M125 119c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M116 86.34c2.33.83 4 3.05 4 5.66 0 3.3-2.7 6-6 6s-6-2.7-6-6c0-2.6 1.67-4.83 4-5.66V72h4v14.34zM128 66c5.52 0 10 4.48 10 10v12h-4V76c0-3.3-2.7-6-6-6v1.83c0 .55-.45 1-1 1-.24 0-.47-.1-.65-.24l-4.46-3.87c-.46-.36-.5-1-.15-1.4.03-.05.07-.1.1-.12l4.47-3.82c.42-.35 1.05-.3 1.4.1.16.2.25.43.25.66V66zm-14 28c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#FC6D26" fill-rule="nonzero" d="M114 74c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm22 28c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#000" fill-opacity=".03" d="M2.12 52C2.04 53 2 54 2 55c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 71.03 58.42 86 39 86S3.65 71.03 2.12 52z"/><path fill="#EEE" fill-rule="nonzero" d="M39 88C17.46 88 0 70.54 0 49s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 14 39 14 4 29.67 4 49s15.67 35 35 35z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M48 41h-4c0-2.76-2.24-5-5-5s-5 2.24-5 5h-4a9 9 0 0 1 18 0zm-18 0h4v3h-4v-3zm14 0h4v3h-4v-3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 47c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V48c0-.55-.45-1-1-1H30zm0-4h18c2.76 0 5 2.24 5 5v12c0 2.76-2.24 5-5 5H30c-2.76 0-5-2.24-5-5V48c0-2.76 2.24-5 5-5z"/><path fill="#6B4FBB" d="M38 53.73c-.6-.34-1-1-1-1.73 0-1.1.9-2 2-2s2 .9 2 2c0 .74-.4 1.4-1 1.73V55c0 .55-.45 1-1 1s-1-.45-1-1v-1.27z"/><path fill="#000" fill-opacity=".03" d="M254.12 92c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M291 128c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#6B4BBE" fill-rule="nonzero" d="M292 78c5.52 0 10 4.48 10 10 0 2.28-.76 4.43-2.14 6.18-1.03 1.3-.8 3.2.5 4.22 1.3 1.02 3.2.8 4.2-.5 2.22-2.8 3.44-6.26 3.44-9.9 0-8.84-7.16-16-16-16v-3.13c0-.2-.06-.4-.17-.56-.3-.42-.93-.54-1.38-.23l-9.2 6.13c-.1.06-.2.16-.28.27-.3.45-.18 1.08.28 1.38l9.2 6.13c.16.1.35.17.55.17.55 0 1-.45 1-1V78z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M290 100c-5.52 0-10-4.48-10-10 0-2.25.74-4.38 2.1-6.12 1-1.3.77-3.2-.54-4.2-1.3-1.02-3.2-.78-4.2.53A15.796 15.796 0 0 0 274 90c0 8.84 7.16 16 16 16v3.13c0 .55.45 1 1 1 .2 0 .4-.06.55-.17l9.2-6.13c.46-.3.6-.93.28-1.38-.07-.1-.17-.2-.28-.28l-9.2-6.13c-.45-.3-1.08-.2-1.38.27-.1.2-.17.4-.17.6v3.1z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M30.24 27.823A14.98 14.98 0 0 0 24 40c0 2.549.636 4.949 1.757 7.051-.297-2.684.644-4.026 2.823-4.026 3.707 0 2.462 5.365 4.473 5.761 2.01.396 4.175.396 4.267 3.29.04 1.257-.265 2.157-.917 2.7a15.095 15.095 0 0 0 8.555-1.006c.035-1.91.303-4.941 2.21-5.61 2.373-.833-.55-1.431.734-3.368 1.17-1.762-3.297-5.2 0-4.832 3.477.388 5.044-.816 6.024-1.456a14.903 14.903 0 0 0-1.373-4.94c-.873.4-2.19.465-3.702-.538-.757-.502-1.084-3.944-2.107-3.944-3.823 0-4.065 3.17-5.994 3.944-1.076.431-4.193 3.773-5.614 3.596-1.126-.14-1.071-4.417-2.45-5.166-1.359-.738-2.174-1.948-2.447-3.633zM39 59c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm1 5h10a2 2 0 1 1 0 4H34a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36a8.004 8.004 0 0 1 1.566-3.705c3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846a8.009 8.009 0 0 1 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1a3.997 3.997 0 0 0-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3a3.99 3.99 0 0 0-.784 1.853l-.346 2.36a4.003 4.003 0 0 1-3.942 3.42l-13.08.053a4 4 0 0 1-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268zm-6 0a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268z"/></g></svg>
\ No newline at end of file
<script>
import Flash from '~/flash';
import GitlabSlackService from '../services/gitlab_slack_service';
import * as UrlUtility from '../../lib/utils/url_utility';
export default {
props: {
projects: {
type: Array,
required: false,
default: () => [],
},
isSignedIn: {
type: Boolean,
required: true,
},
gitlabForSlackGifPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
slackLinkPath: {
type: String,
required: true,
},
gitlabLogoPath: {
type: String,
required: true,
},
slackLogoPath: {
type: String,
required: true,
},
docsPath: {
type: String,
required: true,
},
},
data() {
return {
popupOpen: false,
selectedProjectId: this.projects && this.projects.length ? this.projects[0].id : 0,
};
},
computed: {
doubleHeadedArrowSvg() {
return gl.utils.spriteIcon('double-headed-arrow');
},
arrowRightSvg() {
return gl.utils.spriteIcon('arrow-right');
},
hasProjects() {
return this.projects.length > 0;
},
},
methods: {
togglePopup() {
this.popupOpen = !this.popupOpen;
},
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then(response => UrlUtility.redirectTo(response.data.add_to_slack_link))
.catch(() => Flash('Unable to build Slack link.'));
},
},
mounted() {
GitlabSlackService.init();
},
};
</script>
<template>
<div>
<div class="center append-right-default">
<h1>GitLab for Slack</h1>
<p>Track your GitLab projects with GitLab for Slack.</p>
</div>
<div class="append-bottom-20 center" v-once>
<img
class="gitlab-slack-logo"
:src="gitlabLogoPath"></img>
<div
class="gitlab-slack-double-headed-arrow inline prepend-left-20 append-right-20"
v-html="doubleHeadedArrowSvg"></div>
<img
class="gitlab-slack-logo"
:src="slackLogoPath"></img>
</div>
<button
type="button"
class="btn btn-red center-block js-popup-button"
@click="togglePopup">
Add GitLab to Slack
</button>
<div
class="popup gitlab-slack-popup center-block prepend-top-20 text-center js-popup"
v-if="popupOpen">
<div
class="inline"
v-if="isSignedIn && hasProjects">
<strong>Select GitLab project to link with your Slack team</strong>
<select
class="gitlab-slack-project-select js-project-select form-control prepend-top-10 append-bottom-10"
v-model="selectedProjectId">
<option
v-for="project in projects"
:key="project.id"
:value="project.id">
{{ project.name }}
</option>
</select>
<button
type="button"
class="btn btn-red pull-right js-add-button"
@click="addToSlack">
Add to Slack
</button>
</div>
<span
class="js-no-projects"
v-else-if="isSignedIn && !hasProjects">
You don't have any projects available.
</span>
<span v-else>
You have to
<a
class="js-gitlab-slack-sign-in-link"
v-once
:href="signInPath">
log in
</a>
</span>
</div>
<div class="center prepend-top-20 append-bottom-10 append-right-5 prepend-left-5">
<img
v-once
class="gitlab-slack-gif"
:src="gitlabForSlackGifPath">
</div>
<div
class="gitlab-slack-example"
v-once>
<h3 class="center">How it works</h3>
<div class="well gitlab-slack-well center-block">
<code class="code center-block append-bottom-10">/project-name issue show &lt;id&gt;</code>
<span>
<div
class="gitlab-slack-right-arrow inline append-right-5"
v-html="arrowRightSvg"></div>
Shows the issue with id
<strong>&lt;id&gt;</strong>
</span>
</div>
<div class="center">
<a :href="docsPath">
More Slack commands
</a>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import AddGitlabSlackApplication from './components/add_gitlab_slack_application.vue';
function mountAddGitlabSlackApplication() {
const el = document.getElementById('js-add-gitlab-slack-application-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-add-gitlab-slack-application-entry-data');
const initialData = JSON.parse(dataNode.innerHTML);
const AddGitlabSlackApplicationComp = Vue.extend(AddGitlabSlackApplication);
new AddGitlabSlackApplicationComp({
propsData: {
projects: initialData.projects,
isSignedIn: initialData.is_signed_in,
gitlabForSlackGifPath: initialData.gitlab_for_slack_gif_path,
signInPath: initialData.sign_in_path,
slackLinkPath: initialData.slack_link_profile_slack_path,
gitlabLogoPath: initialData.gitlab_logo_path,
slackLogoPath: initialData.slack_logo_path,
docsPath: initialData.docs_path,
},
}).$mount(el);
}
document.addEventListener('DOMContentLoaded', mountAddGitlabSlackApplication);
export default mountAddGitlabSlackApplication;
import axios from 'axios';
import setAxiosCsrfToken from '../../lib/utils/axios_utils';
export default {
init() {
setAxiosCsrfToken();
},
addToSlack(url, projectId) {
return axios.get(url, {
params: {
project_id: projectId,
},
});
},
};
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props, no-new */ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props, no-new */
/* global ProjectSelect */
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import groupsSelect from './groups_select'; import groupsSelect from './groups_select';
import './project_select'; import projectSelect from './project_select';
class AuditLogs { class AuditLogs {
constructor() { constructor() {
...@@ -11,7 +10,7 @@ class AuditLogs { ...@@ -11,7 +10,7 @@ class AuditLogs {
} }
initFilters() { initFilters() {
new ProjectSelect(); projectSelect();
groupsSelect(); groupsSelect();
new UsersSelect(); new UsersSelect();
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import { s__ } from './locale'; import { s__ } from './locale';
/* global ProjectSelect */ import projectSelect from './project_select';
import IssuableIndex from './issuable_index'; import IssuableIndex from './issuable_index';
/* global Milestone */ /* global Milestone */
import IssuableForm from './issuable_form'; import IssuableForm from './issuable_form';
...@@ -27,8 +27,7 @@ import projectAvatar from './project_avatar'; ...@@ -27,8 +27,7 @@ import projectAvatar from './project_avatar';
/* global CompareAutocomplete */ /* global CompareAutocomplete */
/* global PathLocks */ /* global PathLocks */
/* global ProjectFindFile */ /* global ProjectFindFile */
/* global ProjectNew */ import ProjectNew from './project_new';
/* global ProjectShow */
import projectImport from './project_import'; import projectImport from './project_import';
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
...@@ -95,6 +94,8 @@ import Members from './members'; ...@@ -95,6 +94,8 @@ import Members from './members';
import memberExpirationDate from './member_expiration_date'; import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select'; import DueDateSelectors from './due_date_select';
import Diff from './diff'; import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription';
import ProjectVariables from './project_variables';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
...@@ -212,7 +213,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -212,7 +213,7 @@ import initGroupAnalytics from './init_group_analytics';
initIssuableSidebar(); initIssuableSidebar();
break; break;
case 'dashboard:milestones:index': case 'dashboard:milestones:index':
new ProjectSelect(); projectSelect();
break; break;
case 'projects:milestones:show': case 'projects:milestones:show':
new UserCallout(); new UserCallout();
...@@ -223,7 +224,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -223,7 +224,7 @@ import initGroupAnalytics from './init_group_analytics';
break; break;
case 'dashboard:issues': case 'dashboard:issues':
case 'dashboard:merge_requests': case 'dashboard:merge_requests':
new ProjectSelect(); projectSelect();
initLegacyFilters(); initLegacyFilters();
break; break;
case 'groups:issues': case 'groups:issues':
...@@ -232,7 +233,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -232,7 +233,7 @@ import initGroupAnalytics from './init_group_analytics';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
new ProjectSelect(); projectSelect();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new Todos();
...@@ -530,7 +531,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -530,7 +531,7 @@ import initGroupAnalytics from './init_group_analytics';
if ($el.find('.dropdown-group-label').length) { if ($el.find('.dropdown-group-label').length) {
new GroupLabelSubscription($el); new GroupLabelSubscription($el);
} else { } else {
new gl.ProjectLabelSubscription($el); new ProjectLabelSubscription($el);
} }
}); });
break; break;
...@@ -579,7 +580,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -579,7 +580,7 @@ import initGroupAnalytics from './init_group_analytics';
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
case 'groups:settings:ci_cd:show': case 'groups:settings:ci_cd:show':
new gl.ProjectVariables(); new ProjectVariables();
break; break;
case 'ci:lints:create': case 'ci:lints:create':
case 'ci:lints:show': case 'ci:lints:show':
...@@ -706,7 +707,6 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -706,7 +707,6 @@ import initGroupAnalytics from './init_group_analytics';
case 'show': case 'show':
new Star(); new Star();
new ProjectNew(); new ProjectNew();
new ProjectShow();
new NotificationsDropdown(); new NotificationsDropdown();
break; break;
case 'wikis': case 'wikis':
......
...@@ -73,11 +73,6 @@ import './pager'; ...@@ -73,11 +73,6 @@ import './pager';
import './preview_markdown'; import './preview_markdown';
import './project_find_file'; import './project_find_file';
import './project_import'; import './project_import';
import './project_label_subscription';
import './project_new';
import './project_select';
import './project_show';
import './project_variables';
import './projects_dropdown'; import './projects_dropdown';
import './projects_list'; import './projects_list';
import './syntax_highlight'; import './syntax_highlight';
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import projectSelect from './project_select';
export default class Project { export default class Project {
constructor() { constructor() {
...@@ -58,7 +58,7 @@ export default class Project { ...@@ -58,7 +58,7 @@ export default class Project {
} }
static projectSelectDropdown() { static projectSelectDropdown() {
new ProjectSelect(); projectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
} }
......
/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */ export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
this.$buttons = this.$container.find('.js-subscribe-button');
(function(global) { this.$buttons.on('click', this.toggleSubscription.bind(this));
class ProjectLabelSubscription { }
constructor(container) {
this.$container = $(container);
this.$buttons = this.$container.find('.js-subscribe-button');
this.$buttons.on('click', this.toggleSubscription.bind(this));
}
toggleSubscription(event) { toggleSubscription(event) {
event.preventDefault(); event.preventDefault();
const $btn = $(event.currentTarget); const $btn = $(event.currentTarget);
const $span = $btn.find('span'); const $span = $btn.find('span');
const url = $btn.attr('data-url'); const url = $btn.attr('data-url');
const oldStatus = $btn.attr('data-status'); const oldStatus = $btn.attr('data-status');
$btn.addClass('disabled'); $btn.addClass('disabled');
$span.toggleClass('hidden'); $span.toggleClass('hidden');
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url url,
}).done(() => { }).done(() => {
let newStatus, newAction; let newStatus;
let newAction;
if (oldStatus === 'unsubscribed') { if (oldStatus === 'unsubscribed') {
[newStatus, newAction] = ['subscribed', 'Unsubscribe']; [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
} else { } else {
[newStatus, newAction] = ['unsubscribed', 'Subscribe']; [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
} }
$span.toggleClass('hidden'); $span.toggleClass('hidden');
$btn.removeClass('disabled'); $btn.removeClass('disabled');
this.$buttons.attr('data-status', newStatus); this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction); this.$buttons.find('> span').text(newAction);
this.$buttons.map((button) => { this.$buttons.map((button) => {
const $button = $(button); const $button = $(button);
if ($button.attr('data-original-title')) { if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
} }
return button; return button;
});
}); });
} });
} }
}
global.ProjectLabelSubscription = ProjectLabelSubscription;
})(window.gl || (window.gl = {}));
This diff is collapsed.
...@@ -2,79 +2,73 @@ ...@@ -2,79 +2,73 @@
import Api from './api'; import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button'; import ProjectSelectComboButton from './project_select_combo_button';
(function () { export default function projectSelect() {
this.ProjectSelect = (function () { $('.ajax-project-select').each(function(i, select) {
function ProjectSelect() { var placeholder;
$('.ajax-project-select').each(function(i, select) { const simpleFilter = $(select).data('simple-filter') || false;
var placeholder; this.groupId = $(select).data('group-id');
const simpleFilter = $(select).data('simple-filter') || false; this.includeGroups = $(select).data('include-groups');
this.groupId = $(select).data('group-id'); this.allProjects = $(select).data('all-projects') || false;
this.includeGroups = $(select).data('include-groups'); this.orderBy = $(select).data('order-by') || 'id';
this.allProjects = $(select).data('all-projects') || false; this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.orderBy = $(select).data('order-by') || 'id'; this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
placeholder = "Search for project"; placeholder = "Search for project";
if (this.includeGroups) { if (this.includeGroups) {
placeholder += " or group"; placeholder += " or group";
} }
$(select).select2({ $(select).select2({
placeholder: placeholder, placeholder: placeholder,
minimumInputLength: 0, minimumInputLength: 0,
query: (function (_this) { query: (function (_this) {
return function (query) { return function (query) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
finalCallback = function (projects) { finalCallback = function (projects) {
var data;
data = {
results: projects
};
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data; var data;
data = { data = groups.concat(projects);
results: projects return finalCallback(data);
};
return query.callback(data);
}; };
if (_this.includeGroups) { return Api.groups(query.term, {}, groupsCallback);
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
}; };
})(this), } else {
id: function(project) { projectsCallback = finalCallback;
if (simpleFilter) return project.id; }
return JSON.stringify({ if (_this.groupId) {
name: project.name, return Api.groupProjects(_this.groupId, query.term, projectsCallback);
url: project.web_url, } else {
}); return Api.projects(query.term, {
}, order_by: _this.orderBy,
text: function (project) { with_issues_enabled: _this.withIssuesEnabled,
return project.name_with_namespace || project.name; with_merge_requests_enabled: _this.withMergeRequestsEnabled,
}, membership: !_this.allProjects,
dropdownCssClass: "ajax-project-dropdown" }, projectsCallback);
}
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
}); });
if (simpleFilter) return select; },
return new ProjectSelectComboButton(select); text: function (project) {
}); return project.name_with_namespace || project.name;
} },
dropdownCssClass: "ajax-project-dropdown"
return ProjectSelect; });
})(); if (simpleFilter) return select;
}).call(window); return new ProjectSelectComboButton(select);
});
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
return ProjectShow;
})();
}).call(window);
// I kept class for future
(() => {
const HIDDEN_VALUE_TEXT = '******';
class ProjectVariables { const HIDDEN_VALUE_TEXT = '******';
constructor() {
this.$revealBtn = $('.js-btn-toggle-reveal-values'); export default class ProjectVariables {
this.$revealBtn.on('click', this.toggleRevealState.bind(this)); constructor() {
} this.$revealBtn = $('.js-btn-toggle-reveal-values');
this.$revealBtn.on('click', this.toggleRevealState.bind(this));
}
toggleRevealState(e) { toggleRevealState(e) {
e.preventDefault(); e.preventDefault();
const oldStatus = this.$revealBtn.attr('data-status'); const oldStatus = this.$revealBtn.attr('data-status');
let newStatus = 'hidden'; let newStatus = 'hidden';
let newAction = 'Reveal Values'; let newAction = 'Reveal Values';
if (oldStatus === 'hidden') { if (oldStatus === 'hidden') {
newStatus = 'revealed'; newStatus = 'revealed';
newAction = 'Hide Values'; newAction = 'Hide Values';
} }
this.$revealBtn.attr('data-status', newStatus); this.$revealBtn.attr('data-status', newStatus);
const $variables = $('.variable-value'); const $variables = $('.variable-value');
$variables.each((_, variable) => { $variables.each((_, variable) => {
const $variable = $(variable); const $variable = $(variable);
let newText = HIDDEN_VALUE_TEXT; let newText = HIDDEN_VALUE_TEXT;
if (newStatus === 'revealed') { if (newStatus === 'revealed') {
newText = $variable.attr('data-value'); newText = $variable.attr('data-value');
} }
$variable.text(newText); $variable.text(newText);
}); });
this.$revealBtn.text(newAction); this.$revealBtn.text(newAction);
}
} }
}
window.gl = window.gl || {};
window.gl.ProjectVariables = ProjectVariables;
})();
...@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
} }
} }
.new-epic-dropdown {
.dropdown-menu {
padding-left: $gl-padding-top;
padding-right: $gl-padding-top;
}
.form-control {
width: 100%;
}
.btn-save {
display: flex;
margin-top: $gl-btn-padding;
}
}
.empty-state .new-epic-dropdown {
display: inline-flex;
.btn-save {
margin-left: 0;
margin-bottom: 0;
}
.btn-new {
margin: 0;
}
}
...@@ -208,3 +208,31 @@ ...@@ -208,3 +208,31 @@
margin-left: -$size; margin-left: -$size;
} }
} }
@mixin triangle($color, $border-color, $size, $border-size) {
&::before,
&::after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: '';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&::before {
border-color: transparent;
border-bottom-color: $border-color;
border-width: ($size + $border-size);
margin-left: -($size + $border-size);
}
&::after {
border-color: transparent;
border-bottom-color: $color;
border-width: $size;
margin-left: -$size;
}
}
...@@ -742,6 +742,17 @@ Image Commenting cursor ...@@ -742,6 +742,17 @@ Image Commenting cursor
$image-comment-cursor-left-offset: 12; $image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30; $image-comment-cursor-top-offset: 30;
/*
Add GitLab Slack Application
*/
$add-to-slack-popup-max-width: 400px;
$add-to-slack-gif-max-width: 850px;
$add-to-slack-well-max-width: 750px;
$add-to-slack-logo-size: 100px;
$double-headed-arrow-width: 100px;
$double-headed-arrow-height: 25px;
$right-arrow-size: 16px;
/* /*
Popup Popup
*/ */
......
...@@ -420,3 +420,41 @@ table.u2f-registrations { ...@@ -420,3 +420,41 @@ table.u2f-registrations {
} }
} }
} }
.gitlab-slack-gif {
width: 100%;
max-width: $add-to-slack-gif-max-width;
}
.gitlab-slack-well {
background-color: $white-light;
box-shadow: none;
max-width: $add-to-slack-well-max-width;
}
.gitlab-slack-logo {
width: $add-to-slack-logo-size;
height: $add-to-slack-logo-size;
}
.gitlab-slack-popup {
width: 100%;
max-width: $add-to-slack-popup-max-width;
}
.gitlab-slack-right-arrow svg {
fill: $white-dark;
width: $right-arrow-size;
height: $right-arrow-size;
vertical-align: text-bottom;
}
.gitlab-slack-double-headed-arrow {
vertical-align: text-top;
svg {
fill: $gray-darker;
width: $double-headed-arrow-width;
height: $double-headed-arrow-height;
}
}
...@@ -54,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -54,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private private
def set_application_setting def set_application_setting
@application_setting = current_application_settings @application_setting = ApplicationSetting.current
end end
def application_setting_params def application_setting_params
......
class Admin::GroupsController < Admin::ApplicationController class Admin::GroupsController < Admin::ApplicationController
prepend EE::Admin::GroupsController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index def index
...@@ -68,10 +70,10 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -68,10 +70,10 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(group_params_ce << group_params_ee) params.require(:group).permit(allowed_group_params)
end end
def group_params_ce def allowed_group_params
[ [
:avatar, :avatar,
:description, :description,
...@@ -84,12 +86,4 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -84,12 +86,4 @@ class Admin::GroupsController < Admin::ApplicationController
:two_factor_grace_period :two_factor_grace_period
] ]
end end
def group_params_ee
[
:repository_size_limit,
:shared_runners_minutes_limit,
:plan_id
]
end
end end
class Admin::UsersController < Admin::ApplicationController class Admin::UsersController < Admin::ApplicationController
prepend EE::Admin::UsersController
before_action :user, except: [:index, :new, :create] before_action :user, except: [:index, :new, :create]
def index def index
...@@ -187,10 +189,10 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -187,10 +189,10 @@ class Admin::UsersController < Admin::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(user_params_ce << user_params_ee) params.require(:user).permit(allowed_user_params)
end end
def user_params_ce def allowed_user_params
[ [
:access_level, :access_level,
:avatar, :avatar,
...@@ -218,13 +220,6 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -218,13 +220,6 @@ class Admin::UsersController < Admin::ApplicationController
] ]
end end
def user_params_ee
[
:note,
namespace_attributes: [:id, :shared_runners_minutes_limit, :plan_id]
]
end
def update_user(&block) def update_user(&block)
result = Users::UpdateService.new(current_user, user: user).execute(&block) result = Users::UpdateService.new(current_user, user: user).execute(&block)
......
...@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base ...@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
before_action :authenticate_user_from_personal_access_token! before_action :authenticate_sessionless_user!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
...@@ -100,27 +99,11 @@ class ApplicationController < ActionController::Base ...@@ -100,27 +99,11 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user) return try(:authenticated_user)
end end
def authenticate_user_from_personal_access_token! # This filter handles personal access tokens, and atom requests with rss tokens
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence def authenticate_sessionless_user!
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
return unless token.present? sessionless_sign_in(user) if user
user = User.find_by_personal_access_token(token)
sessionless_sign_in(user)
end
# This filter handles authentication for atom request with an rss_token
def authenticate_user_from_rss_token!
return unless request.format.atom?
token = params[:rss_token].presence
return unless token.present?
user = User.find_by_rss_token(token)
sessionless_sign_in(user)
end end
def verify_namespace_plan_check_enabled def verify_namespace_plan_check_enabled
......
...@@ -11,6 +11,7 @@ module LfsRequest ...@@ -11,6 +11,7 @@ module LfsRequest
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
prepend EE::LfsRequest
before_action :require_lfs_enabled! before_action :require_lfs_enabled!
before_action :lfs_check_access! before_action :lfs_check_access!
end end
...@@ -103,40 +104,4 @@ module LfsRequest ...@@ -103,40 +104,4 @@ module LfsRequest
def has_authentication_ability?(capability) def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability) (authentication_abilities || []).include?(capability)
end end
module EE
def lfs_forbidden!
raise NotImplementedError unless defined?(super)
if project.above_size_limit? || objects_exceed_repo_limit?
render_size_error
else
super
end
end
def render_size_error
render(
json: {
message: Gitlab::RepositorySizeError.new(project).push_error(@exceeded_limit),
documentation_url: help_url
},
content_type: "application/vnd.git-lfs+json",
status: 406
)
end
def objects_exceed_repo_limit?
return false unless project.size_limit_enabled?
return @limit_exceeded if defined?(@limit_exceeded)
lfs_push_size = objects.sum { |o| o[:size] }
size_with_lfs_push = project.repository_and_lfs_size + lfs_push_size
@exceeded_limit = size_with_lfs_push - project.actual_size_limit
@limit_exceeded = @exceeded_limit > 0
end
end
prepend EE
end end
module ServiceParams module ServiceParams
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepend EE::ServiceParams
ALLOWED_PARAMS_CE = [ ALLOWED_PARAMS_CE = [
:active, :active,
...@@ -62,19 +63,12 @@ module ServiceParams ...@@ -62,19 +63,12 @@ module ServiceParams
:webhook :webhook
].freeze ].freeze
ALLOWED_PARAMS_EE = [
:jenkins_url,
:multiproject_enabled,
:pass_unstable,
:project_name
].freeze
# Parameters to ignore if no value is specified # Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password].freeze FILTER_BLANK_PARAMS = [:password].freeze
def service_params def service_params
dynamic_params = @service.event_channel_names + @service.event_names dynamic_params = @service.event_channel_names + @service.event_names
service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + ALLOWED_PARAMS_EE + dynamic_params) service_params = params.permit(:id, service: allowed_service_params + dynamic_params)
if service_params[:service].is_a?(Hash) if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param| FILTER_BLANK_PARAMS.each do |param|
...@@ -84,4 +78,8 @@ module ServiceParams ...@@ -84,4 +78,8 @@ module ServiceParams
service_params service_params
end end
def allowed_service_params
ALLOWED_PARAMS_CE
end
end end
...@@ -57,7 +57,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -57,7 +57,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user if current_user
log_audit_event(current_user, with: :saml) log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed. # Update SAML identity if data has changed.
identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml) identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil? if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated' redirect_to profile_account_path, notice: 'Authentication method updated'
...@@ -112,7 +112,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -112,7 +112,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth def handle_omniauth
if current_user if current_user
# Add new authentication method # Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) current_user.identities
.with_extern_uid(oauth['provider'], oauth['uid'])
.first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider']) log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated' redirect_to profile_account_path, notice: 'Authentication method updated'
else else
......
...@@ -74,7 +74,11 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -74,7 +74,11 @@ class Projects::WikisController < Projects::ApplicationController
def history def history
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
unless @page if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
total_count: @page.count_versions)
.page(params[:page])
else
redirect_to( redirect_to(
project_wiki_path(@project, :home), project_wiki_path(@project, :home),
notice: "Page not found" notice: "Page not found"
...@@ -102,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -102,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project) redirect_to project_path(@project)
......
module Geo
class ExpireUploadsFinder
def find_project_uploads(project)
if Gitlab::Geo.fdw?
fdw_find_project_uploads(project)
else
legacy_find_project_uploads(project)
end
end
def find_file_registries_uploads(project)
if Gitlab::Geo.fdw?
fdw_find_file_registries_uploads(project)
else
legacy_find_file_registries_uploads(project)
end
end
#
# FDW accessors
#
# @return [ActiveRecord::Relation<Geo::Fdw::Upload>]
def fdw_find_project_uploads(project)
fdw_table = Geo::Fdw::Upload.table_name
upload_type = 'file'
Geo::Fdw::Upload.joins("JOIN file_registry
ON file_registry.file_id = #{fdw_table}.id
AND #{fdw_table}.model_id='#{project.id}'
AND #{fdw_table}.model_type='#{project.class.name}'
AND file_registry.file_type='#{upload_type}'")
end
# @return [ActiveRecord::Relation<Geo::FileRegistry>]
def fdw_find_file_registries_uploads(project)
fdw_table = Geo::Fdw::Upload.table_name
upload_type = 'file'
Geo::FileRegistry.joins("JOIN #{fdw_table}
ON file_registry.file_id = #{fdw_table}.id
AND #{fdw_table}.model_id='#{project.id}'
AND #{fdw_table}.model_type='#{project.class.name}'
AND file_registry.file_type='#{upload_type}'")
end
#
# Legacy accessors (non FDW)
#
# @return [ActiveRecord::Relation<Geo::FileRegistry>] list of file registry items
def legacy_find_file_registries_uploads(project)
upload_ids = Upload.where(model_type: project.class.name, model_id: project.id).pluck(:id)
return Geo::FileRegistry.none if upload_ids.empty?
values_sql = upload_ids.map { |id| "(#{id})" }.join(',')
upload_type = 'file'
Geo::FileRegistry.joins(<<~SQL)
JOIN (VALUES #{values_sql})
AS uploads (id)
ON uploads.id = file_registry.file_id
AND file_registry.file_type='#{upload_type}'
SQL
end
# @return [ActiveRecord::Relation<Upload>] list of upload files
def legacy_find_project_uploads(project)
file_registry_ids = legacy_find_file_registries_uploads(project).pluck(:file_id)
return Upload.none if file_registry_ids.empty?
values_sql = file_registry_ids.map { |f_id| "(#{f_id})" }.join(',')
Upload.joins(<<~SQL)
JOIN (VALUES #{values_sql})
AS file_registry (file_id)
ON (file_registry.file_id = uploads.id)
SQL
end
end
end
module Geo
class RegistryFinder
attr_reader :current_node
def initialize(current_node: nil)
@current_node = current_node
end
def find_failed_objects(batch_size:)
Geo::FileRegistry
.failed
.retry_due
.limit(batch_size)
.pluck(:file_id, :file_type)
end
# Find limited amount of non replicated lfs objects.
#
# You can pass a list with `except_registry_ids:` so you can exclude items you
# already scheduled but haven't finished and persisted to the database yet
#
# TODO: Alternative here is to use some sort of window function with a cursor instead
# of simply limiting the query and passing a list of items we don't want
#
# @param [Integer] batch_size used to limit the results returned
# @param [Array<Integer>] except_registry_ids ids that will be ignored from the query
def find_nonreplicated_lfs_objects(batch_size:, except_registry_ids:)
# Selective project replication adds a wrinkle to FDW queries, so
# we fallback to the legacy version for now.
relation =
if Gitlab::Geo.fdw? && !selective_sync
fdw_find_nonreplicated_lfs_objects
else
legacy_find_nonreplicated_lfs_objects(except_registry_ids: except_registry_ids)
end
relation
.limit(batch_size)
.pluck(:id)
.map { |id| [id, :lfs] }
end
# Find limited amount of non replicated uploads.
#
# You can pass a list with `except_registry_ids:` so you can exclude items you
# already scheduled but haven't finished and persisted to the database yet
#
# TODO: Alternative here is to use some sort of window function with a cursor instead
# of simply limiting the query and passing a list of items we don't want
#
# @param [Integer] batch_size used to limit the results returned
# @param [Array<Integer>] except_registry_ids ids that will be ignored from the query
def find_nonreplicated_uploads(batch_size:, except_registry_ids:)
# Selective project replication adds a wrinkle to FDW queries, so
# we fallback to the legacy version for now.
relation =
if Gitlab::Geo.fdw? && !selective_sync
fdw_find_nonreplicated_uploads
else
legacy_find_nonreplicated_uploads(except_registry_ids: except_registry_ids)
end
relation
.limit(batch_size)
.pluck(:id, :uploader)
.map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').underscore] }
end
protected
def selective_sync
current_node.restricted_project_ids
end
#
# FDW accessors
#
def fdw_find_nonreplicated_lfs_objects
fdw_table = Geo::Fdw::LfsObject.table_name
# Filter out objects in object storage (this is done in GeoNode#lfs_objects)
Geo::Fdw::LfsObject.joins("LEFT OUTER JOIN file_registry
ON file_registry.file_id = #{fdw_table}.id
AND file_registry.file_type = 'lfs'")
.where("#{fdw_table}.file_store IS NULL OR #{fdw_table}.file_store = #{LfsObjectUploader::LOCAL_STORE}")
.where('file_registry.file_id IS NULL')
end
def fdw_find_nonreplicated_uploads
fdw_table = Geo::Fdw::Upload.table_name
upload_types = Geo::FileService::DEFAULT_OBJECT_TYPES.map { |val| "'#{val}'" }.join(',')
Geo::Fdw::Upload.joins("LEFT OUTER JOIN file_registry
ON file_registry.file_id = #{fdw_table}.id
AND file_registry.file_type IN (#{upload_types})")
.where('file_registry.file_id IS NULL')
end
#
# Legacy accessors (non FDW)
#
def legacy_find_nonreplicated_lfs_objects(except_registry_ids:)
registry_ids = legacy_pluck_registry_ids(file_types: :lfs, except_registry_ids: except_registry_ids)
legacy_filter_registry_ids(
current_node.lfs_objects,
registry_ids,
LfsObject.table_name
)
end
def legacy_find_nonreplicated_uploads(except_registry_ids:)
registry_ids = legacy_pluck_registry_ids(file_types: Geo::FileService::DEFAULT_OBJECT_TYPES, except_registry_ids: except_registry_ids)
legacy_filter_registry_ids(
current_node.uploads,
registry_ids,
Upload.table_name
)
end
# This query requires data from two different databases, and unavoidably
# plucks a list of file IDs from one into the other. This will not scale
# well with the number of synchronized files--the query will increase
# linearly in size--so this should be replaced with postgres_fdw ASAP.
def legacy_filter_registry_ids(objects, registry_ids, table_name)
return objects if registry_ids.empty?
joined_relation = objects.joins(<<~SQL)
LEFT OUTER JOIN
(VALUES #{registry_ids.map { |id| "(#{id}, 't')" }.join(',')})
file_registry(file_id, registry_present)
ON #{table_name}.id = file_registry.file_id
SQL
joined_relation.where(file_registry: { registry_present: [nil, false] })
end
def legacy_pluck_registry_ids(file_types:, except_registry_ids:)
ids = Geo::FileRegistry.where(file_type: file_types).pluck(:file_id)
(ids + except_registry_ids).uniq
end
end
end
...@@ -232,6 +232,15 @@ module ApplicationSettingsHelper ...@@ -232,6 +232,15 @@ module ApplicationSettingsHelper
:sign_in_text, :sign_in_text,
:signup_enabled, :signup_enabled,
:terminal_max_session_time, :terminal_max_session_time,
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_period_in_seconds,
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_requests_per_period,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_api_enabled,
:throttle_authenticated_api_requests_per_period,
:throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period, :two_factor_grace_period,
:unique_ips_limit_enabled, :unique_ips_limit_enabled,
:unique_ips_limit_per_user, :unique_ips_limit_per_user,
......
module ServicesHelper module ServicesHelper
prepend EE::ServicesHelper
def service_event_description(event) def service_event_description(event)
case event case event
when "push", "push_events" when "push", "push_events"
......
...@@ -308,6 +308,15 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -308,6 +308,15 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil, sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0, terminal_max_session_time: 0,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_requests_per_period: 3600,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_requests_per_period: 7200,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_requests_per_period: 7200,
throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, user_default_external: false,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
......
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Importable include Importable
prepend EE::Build prepend EE::Ci::Build
belongs_to :runner belongs_to :runner
belongs_to :trigger_request belongs_to :trigger_request
...@@ -40,7 +40,6 @@ module Ci ...@@ -40,7 +40,6 @@ module Ci
scope :with_artifacts_stored_locally, ->() { with_artifacts.where(artifacts_file_store: [nil, ArtifactUploader::LOCAL_STORE]) } scope :with_artifacts_stored_locally, ->() { with_artifacts.where(artifacts_file_store: [nil, ArtifactUploader::LOCAL_STORE]) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :codequality, ->() { where(name: %w[codequality codeclimate]) }
scope :ref_protected, -> { where(protected: true) } scope :ref_protected, -> { where(protected: true) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
...@@ -475,11 +474,6 @@ module Ci ...@@ -475,11 +474,6 @@ module Ci
trace trace
end end
def has_codeclimate_json?
options.dig(:artifacts, :paths) == ['codeclimate.json'] &&
artifacts_metadata?
end
def serializable_hash(options = {}) def serializable_hash(options = {})
super(options).merge(when: read_attribute(:when)) super(options).merge(when: read_attribute(:when))
end end
......
...@@ -481,10 +481,6 @@ module Ci ...@@ -481,10 +481,6 @@ module Ci
.fabricate! .fabricate!
end end
def codeclimate_artifact
artifacts.codequality.find(&:has_codeclimate_json?)
end
def latest_builds_with_artifacts def latest_builds_with_artifacts
@latest_builds_with_artifacts ||= builds.latest.with_artifacts @latest_builds_with_artifacts ||= builds.latest.with_artifacts
end end
......
...@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base ...@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base
belongs_to :forked_from_project, class_name: 'Project' belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true validates :fork_network, :project, presence: true
after_destroy :cleanup_fork_network
private
def cleanup_fork_network
# Explicitly using `#count` makes sure we have the correct number if the
# relation was loaded in the fork_network.
fork_network.destroy if fork_network.fork_network_members.count == 0
end
end end
class Geo::DeletedProject < ::Project class Geo::DeletedProject
after_initialize :readonly! include Gitlab::CurrentSettings
def initialize(id:, name:, full_path:, repository_storage:) attr_reader :id, :name, :disk_path
repository_storage ||= current_application_settings.pick_repository_storage
super(id: id, name: name, repository_storage: repository_storage) def initialize(id:, name:, disk_path:, repository_storage:)
@full_path = full_path @id = id
@name = name
@disk_path = disk_path
@repository_storage = repository_storage
end end
def full_path alias_method :full_path, :disk_path
@full_path
def repository
@repository ||= Repository.new(disk_path, self)
end
def repository_storage
@repository_storage ||= current_application_settings.pick_repository_storage
end
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
def wiki
@wiki ||= ProjectWiki.new(self, nil)
end
def wiki_path
wiki.disk_path
end
# When we remove project we move the repository to path+deleted.git then
# outside the transaction we schedule removal of path+deleted with Sidekiq
# through the run_after_commit callback. In a Geo secondary node, we don't
# attempt to remove the repositories inside a transaction because we don't
# have access to the original model anymore, we just need to perform some
# cleanup. This method will run the given block to remove repositories
# immediately otherwise will leave us with stalled repositories on disk.
def run_after_commit(&block)
instance_eval(&block)
end end
alias_method :path_with_namespace, :full_path
end end
...@@ -37,7 +37,7 @@ class Group < Namespace ...@@ -37,7 +37,7 @@ class Group < Namespace
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy` # We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`. # here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, dependent: :delete_all, foreign_key: 'entity_id' # rubocop:disable Cop/ActiveRecordDependent has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
......
...@@ -635,6 +635,8 @@ class Project < ActiveRecord::Base ...@@ -635,6 +635,8 @@ class Project < ActiveRecord::Base
else else
super super
end end
rescue
super
end end
def valid_import_url? def valid_import_url?
......
...@@ -85,8 +85,8 @@ class ProjectWiki ...@@ -85,8 +85,8 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an # Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages. # empty Array if this Wiki has no pages.
def pages def pages(limit: nil)
wiki.pages.map { |page| WikiPage.new(self, page, true) } wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end end
# Finds a page within the repository based on a tile # Finds a page within the repository based on a tile
......
...@@ -127,19 +127,24 @@ class WikiPage ...@@ -127,19 +127,24 @@ class WikiPage
@version ||= @page.version @version ||= @page.version
end end
# Returns an array of Gitlab Commit instances. def versions(options = {})
def versions
return [] unless persisted? return [] unless persisted?
wiki.wiki.page_versions(@page.path) wiki.wiki.page_versions(@page.path, options)
end end
def commit def count_versions
versions.first return [] unless persisted?
wiki.wiki.count_page_versions(@page.path)
end
def last_version
@last_version ||= versions(limit: 1).first
end end
def last_commit_sha def last_commit_sha
commit&.sha last_version&.sha
end end
# Returns the Date that this latest version was # Returns the Date that this latest version was
...@@ -151,7 +156,7 @@ class WikiPage ...@@ -151,7 +156,7 @@ class WikiPage
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
# is an old version of the page. # is an old version of the page.
def historical? def historical?
@page.historical? && versions.first.sha != version.sha @page.historical? && last_version.sha != version.sha
end end
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
......
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
include TimeTrackableEntity include TimeTrackableEntity
prepend ::EE::MergeRequestEntity
expose :state expose :state
expose :deleted_at expose :deleted_at
...@@ -189,29 +190,6 @@ class MergeRequestEntity < IssuableEntity ...@@ -189,29 +190,6 @@ class MergeRequestEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request) commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end end
# EE-specific
expose :codeclimate, if: -> (mr, _) { mr.has_codeclimate_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :head_blob_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
expose :base_path, if: -> (mr, _) { can?(current_user, :read_build, mr.base_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :base_blob_path, if: -> (mr, _) { mr.base_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.base_pipeline_sha)
end
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request
......
module Geo
class FilesExpireService
include ::Gitlab::Geo::LogHelpers
BATCH_SIZE = 500
attr_reader :project, :old_full_path
def initialize(project, old_full_path)
@project = project
@old_full_path = old_full_path
end
# Expire already replicated uploads
#
# This is a fallback solution to support projects that haven't rolled out to hashed-storage yet.
#
# Note: Unless we add some locking mechanism, this will be best effort only
# as if there are files that are being replicated during this execution, they will not
# be expired.
#
# The long-term solution is to use hashed storage.
def execute
return unless Gitlab::Geo.secondary?
uploads = finder.find_project_uploads(project)
log_info("Expiring replicated attachments after project rename", count: uploads.count)
schedule_file_removal(uploads)
mark_for_resync!
end
# Project's base directory for attachments storage
#
# @return base directory where all uploads for the project are stored
def base_dir
@base_dir ||= File.join(CarrierWave.root, FileUploader.base_dir, old_full_path)
end
private
def schedule_file_removal(uploads)
paths_to_remove = uploads.find_each(batch_size: BATCH_SIZE).reduce([]) do |to_remove, upload|
file_path = File.join(base_dir, upload.path)
if File.exist?(file_path)
to_remove << [file_path]
log_info("Scheduled to remove file", file_path: file_path)
end
to_remove
end
Sidekiq::Client.push_bulk('class' => Geo::FileRemovalWorker, 'args' => paths_to_remove)
end
def mark_for_resync!
finder.find_file_registries_uploads(project).delete_all
end
def finder
@finder ||= ::Geo::ExpireUploadsFinder.new
end
# This is called by LogHelpers to build json log with context info
#
# @see ::Gitlab::Geo::LogHelpers
def base_log_data(message)
{
class: self.class.name,
project_id: project.id,
project_path: project.full_path,
project_old_path: old_full_path,
message: message
}
end
end
end
...@@ -24,6 +24,7 @@ module Geo ...@@ -24,6 +24,7 @@ module Geo
def fetch_geo_node_metrics(node) def fetch_geo_node_metrics(node)
return unless node.enabled? return unless node.enabled?
return unless Gitlab::Geo.primary? || Gitlab::Metrics.prometheus_metrics_enabled?
status = node_status(node) status = node_status(node)
...@@ -33,7 +34,7 @@ module Geo ...@@ -33,7 +34,7 @@ module Geo
end end
update_db_metrics(node, status) if Gitlab::Geo.primary? update_db_metrics(node, status) if Gitlab::Geo.primary?
update_prometheus_metrics(node, status) update_prometheus_metrics(node, status) if Gitlab::Metrics.prometheus_metrics_enabled?
end end
def update_db_metrics(node, status) def update_db_metrics(node, status)
......
...@@ -14,15 +14,29 @@ module Geo ...@@ -14,15 +14,29 @@ module Geo
end end
def execute def execute
project.ensure_storage_path_exists unless move_repositories!
move_project_repository && move_wiki_repository return false
rescue end
log_error('Repository cannot be renamed')
false unless project.hashed_storage?(:attachments)
Geo::FilesExpireService.new(project, old_disk_path).execute
end
true
end end
private private
def move_repositories!
begin
project.ensure_storage_path_exists
move_project_repository && move_wiki_repository
rescue => ex
log_error('Repository cannot be renamed', error: ex)
false
end
end
def move_project_repository def move_project_repository
gitlab_shell.mv_repository(project.repository_storage_path, old_disk_path, new_disk_path) gitlab_shell.mv_repository(project.repository_storage_path, old_disk_path, new_disk_path)
end end
......
module Geo module Geo
class RepositoryDestroyService class RepositoryDestroyService
attr_reader :id, :name, :full_path, :storage_name attr_reader :id, :name, :disk_path, :repository_storage
def initialize(id, name, full_path, storage_name) def initialize(id, name, disk_path, repository_storage)
@id = id @id = id
@name = name @name = name
@full_path = full_path @disk_path = disk_path
@storage_name = storage_name @repository_storage = repository_storage
end end
def async_execute def async_execute
GeoRepositoryDestroyWorker.perform_async(id, name, full_path, storage_name) GeoRepositoryDestroyWorker.perform_async(id, name, disk_path, repository_storage)
end end
def execute def execute
...@@ -24,8 +24,8 @@ module Geo ...@@ -24,8 +24,8 @@ module Geo
# rebuilding only what our service class requires # rebuilding only what our service class requires
::Geo::DeletedProject.new(id: id, ::Geo::DeletedProject.new(id: id,
name: name, name: name,
full_path: full_path, disk_path: disk_path,
repository_storage: storage_name) repository_storage: repository_storage)
end end
end end
end end
...@@ -835,5 +835,56 @@ ...@@ -835,5 +835,56 @@
The amount of seconds after which a request to get a secondary node The amount of seconds after which a request to get a secondary node
status will time out. status will time out.
%fieldset
%legend User and IP Rate Limits
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_unauthenticated_enabled do
= f.check_box :throttle_unauthenticated_enabled
Enable unauthenticated request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_api_enabled do
= f.check_box :throttle_authenticated_api_enabled
Enable authenticated API request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_web_enabled do
= f.check_box :throttle_authenticated_web_enabled
Enable authenticated web request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- epics = EpicsFinder.new(current_user, group_id: @group.id).execute
- epics_items = ['epics#show', 'epics#index']
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards) - if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show') - issues_sub_menu_items.push('boards#index', 'boards#show')
...@@ -45,20 +43,8 @@ ...@@ -45,20 +43,8 @@
%span %span
Contribution Analytics Contribution Analytics
-# TODO: Add the flag check to only show epics if available
= nav_link(path: epics_items) do = render "layouts/nav/ee/epic_link", group: @group
= link_to group_epics_path(@group) do
.nav-icon-container
= sprite_icon('epic')
%span.nav-item-name
Epics
%span.badge.count= number_with_delimiter(epics.count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: epics_items, html_options: { class: "fly-out-top-item" } ) do
= link_to group_epics_path(@group) do
%strong.fly-out-top-item-name
#{ _('Epics') }
%span.badge.count.epic_counter.fly-out-badge= number_with_delimiter(epics.count)
= nav_link(path: issues_sub_menu_items) do = nav_link(path: issues_sub_menu_items) do
= link_to issues_group_path(@group) do = link_to issues_group_path(@group) do
......
= webpack_bundle_tag 'add_gitlab_slack_application'
%script#js-add-gitlab-slack-application-entry-data{ type: "application/json" }
= add_to_slack_data(@projects)
#js-add-gitlab-slack-application-entry-point
%a{ href: "https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_redirect_uri(@project)}" } %a#slack-button{ href: add_to_slack_link(project, slack_app_id) }
%img{ alt:"Add to Slack", height: "40", src: "https://platform.slack-edge.com/img/add_to_slack.png", srcset: "https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x", width: "139" } %img{ alt:"Add to Slack", height: "40", src: "https://platform.slack-edge.com/img/add_to_slack.png", srcset: "https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x", width: "139" }
...@@ -27,4 +27,4 @@ ...@@ -27,4 +27,4 @@
= link_to 'Remove', project_settings_slack_path(project), method: :delete, class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure?' } = link_to 'Remove', project_settings_slack_path(project), method: :delete, class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure?' }
- else - else
%p To set up this service press "Add to Slack" %p To set up this service press "Add to Slack"
= render "projects/services/#{@service.to_param}/slack_button" = render "projects/services/#{@service.to_param}/slack_button", project: @project
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page) = link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format}) %small (#{wiki_page.format})
.pull-right .pull-right
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
%th= _("Last updated") %th= _("Last updated")
%th= _("Format") %th= _("Format")
%tbody %tbody
- @page.versions.each_with_index do |version, index| - @page_versions.each_with_index do |version, index|
- commit = version - commit = version
%tr %tr
%td %td
...@@ -37,5 +37,6 @@ ...@@ -37,5 +37,6 @@
%td %td
%strong %strong
= version.format = version.format
= paginate @page_versions, theme: 'gitlab'
= render 'sidebar' = render 'sidebar'
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
.nav-text .nav-text
%h2.wiki-page-title= @page.title.capitalize %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by %span.wiki-last-edit-by
= (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.commit.authored_date)} #{time_ago_with_tooltip(@page.last_version.authored_date)}
.nav-controls .nav-controls
= render 'main_links' = render 'main_links'
......
...@@ -12,6 +12,13 @@ module Geo ...@@ -12,6 +12,13 @@ module Geo
{ id: object_db_id, type: object_type, job_id: job_id } if job_id { id: object_db_id, type: object_type, job_id: job_id } if job_id
end end
def finder
@finder ||= RegistryFinder.new(current_node: current_node)
end
# Pools for new resources to be transferred
#
# @return [Array] resources to be transferred
def load_pending_resources def load_pending_resources
resources = find_unsynced_objects(batch_size: db_retrieve_batch_size) resources = find_unsynced_objects(batch_size: db_retrieve_batch_size)
remaining_capacity = db_retrieve_batch_size - resources.count remaining_capacity = db_retrieve_batch_size - resources.count
...@@ -19,120 +26,21 @@ module Geo ...@@ -19,120 +26,21 @@ module Geo
if remaining_capacity.zero? if remaining_capacity.zero?
resources resources
else else
resources + find_failed_objects(batch_size: remaining_capacity) resources + finder.find_failed_objects(batch_size: remaining_capacity)
end end
end end
def find_unsynced_objects(batch_size:) def find_unsynced_objects(batch_size:)
lfs_object_ids = find_lfs_object_ids(batch_size: batch_size) lfs_object_ids = finder.find_nonreplicated_lfs_objects(batch_size: batch_size, except_registry_ids: scheduled_file_ids(:lfs))
upload_objects_ids = find_upload_object_ids(batch_size: batch_size) upload_objects_ids = finder.find_nonreplicated_uploads(batch_size: batch_size, except_registry_ids: scheduled_file_ids(Geo::FileService::DEFAULT_OBJECT_TYPES))
interleave(lfs_object_ids, upload_objects_ids) interleave(lfs_object_ids, upload_objects_ids)
end end
def find_failed_objects(batch_size:) def scheduled_file_ids(file_types)
Geo::FileRegistry file_types = Array(file_types) unless file_types.is_a? Array
.failed
.retry_due
.limit(batch_size)
.pluck(:file_id, :file_type)
end
def selective_sync
current_node.restricted_project_ids
end
def find_lfs_object_ids(batch_size:)
# Selective project replication adds a wrinkle to FDW queries, so
# we fallback to the legacy version for now.
relation =
if Gitlab::Geo.fdw? && !selective_sync
fdw_find_lfs_object_ids
else
legacy_find_lfs_object_ids
end
relation
.limit(batch_size)
.pluck(:id)
.map { |id| [id, :lfs] }
end
def find_upload_object_ids(batch_size:)
# Selective project replication adds a wrinkle to FDW queries, so
# we fallback to the legacy version for now.
relation =
if Gitlab::Geo.fdw? && !selective_sync
fdw_find_upload_object_ids
else
legacy_find_upload_object_ids
end
relation
.limit(batch_size)
.pluck(:id, :uploader)
.map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').underscore] }
end
def fdw_find_lfs_object_ids
fdw_table = Geo::Fdw::LfsObject.table_name
# Filter out objects in object storage (this is done in GeoNode#lfs_objects)
Geo::Fdw::LfsObject.joins("LEFT OUTER JOIN file_registry ON file_registry.file_id = #{fdw_table}.id AND file_registry.file_type = 'lfs'")
.where("#{fdw_table}.file_store IS NULL OR #{fdw_table}.file_store = #{LfsObjectUploader::LOCAL_STORE}")
.where('file_registry.file_id IS NULL')
end
def fdw_find_upload_object_ids
fdw_table = Geo::Fdw::Upload.table_name
obj_types = Geo::FileService::DEFAULT_OBJECT_TYPES.map { |val| "'#{val}'" }.join(',')
Geo::Fdw::Upload.joins("LEFT OUTER JOIN file_registry ON file_registry.file_id = #{fdw_table}.id AND file_registry.file_type IN (#{obj_types})")
.where('file_registry.file_id IS NULL')
end
def legacy_find_upload_object_ids
legacy_filter_registry_ids(
current_node.uploads,
Geo::FileService::DEFAULT_OBJECT_TYPES,
Upload.table_name
)
end
def legacy_find_lfs_object_ids
legacy_filter_registry_ids(
current_node.lfs_objects,
[:lfs],
LfsObject.table_name
)
end
# This query requires data from two different databases, and unavoidably
# plucks a list of file IDs from one into the other. This will not scale
# well with the number of synchronized files--the query will increase
# linearly in size--so this should be replaced with postgres_fdw ASAP.
def legacy_filter_registry_ids(objects, file_types, table_name)
registry_ids = legacy_pluck_registry_ids(Geo::FileRegistry, file_types)
return objects if registry_ids.empty?
joined_relation = objects.joins(<<~SQL)
LEFT OUTER JOIN
(VALUES #{registry_ids.map { |id| "(#{id}, 't')" }.join(',')})
file_registry(file_id, registry_present)
ON #{table_name}.id = file_registry.file_id
SQL
joined_relation.where(file_registry: { registry_present: [nil, false] })
end
def legacy_pluck_registry_ids(relation, file_types)
ids = relation.where(file_type: file_types).pluck(:file_id)
(ids + scheduled_file_ids(file_types)).uniq
end
def scheduled_file_ids(types) scheduled_jobs.select { |data| file_types.include?(data[:type]) }.map { |data| data[:id] }
scheduled_jobs.select { |data| types.include?(data[:type]) }.map { |data| data[:id] }
end end
end end
end end
module Geo
class FileRemovalWorker
include Sidekiq::Worker
include Gitlab::Geo::LogHelpers
sidekiq_options queue: :geo
def perform(file_path)
remove_file!(file_path)
end
private
def remove_file!(file_path)
if File.file?(file_path)
begin
File.unlink(file_path)
rescue => ex
log_error("Failed to remove file", ex, file_path: file_path)
end
log_info("Removed file", file_path: file_path)
else
log_info("Tried to remove file, but it was not found", file_path: file_path)
end
end
end
end
...@@ -7,8 +7,6 @@ module Geo ...@@ -7,8 +7,6 @@ module Geo
LEASE_TIMEOUT = 5.minutes LEASE_TIMEOUT = 5.minutes
def perform def perform
return unless Gitlab::Metrics.prometheus_metrics_enabled?
try_obtain_lease { Geo::MetricsUpdateService.new.execute } try_obtain_lease { Geo::MetricsUpdateService.new.execute }
end end
......
...@@ -3,7 +3,7 @@ class GeoRepositoryDestroyWorker ...@@ -3,7 +3,7 @@ class GeoRepositoryDestroyWorker
include GeoQueue include GeoQueue
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
def perform(id, name, full_path, storage_name) def perform(id, name, disk_path, storage_name)
Geo::RepositoryDestroyService.new(id, name, full_path, storage_name).execute Geo::RepositoryDestroyService.new(id, name, disk_path, storage_name).execute
end end
end end
...@@ -2,23 +2,18 @@ ...@@ -2,23 +2,18 @@
# vim: ft=ruby # vim: ft=ruby
require 'rubygems' require 'rubygems'
require 'bundler/setup' require 'bundler/setup'
# loads rails environment / initializers
require "#{File.dirname(__FILE__)}/../config/environment"
require 'optparse' require 'optparse'
class GeoLogCursorOptionParser class GeoLogCursorOptionParser
def self.parse(argv) def self.parse(argv)
options = { full_scan: false } options = {}
version = Gitlab::Geo::LogCursor::Daemon::VERSION
op = OptionParser.new op = OptionParser.new
op.banner = 'GitLab Geo: Log Cursor' op.banner = 'GitLab Geo: Log Cursor'
op.separator '' op.separator ''
op.separator 'Usage: ./geo_log_cursor [options]' op.separator 'Usage: ./geo_log_cursor [options]'
op.separator '' op.separator ''
op.on('-f', '--full-scan', 'Performs full-scan to lookup for un-replicated data') { options[:full_scan] = true } op.on('-d', '--debug', 'Enable detailed logging with extra debug information') { options[:debug] = true }
op.separator 'Common options:' op.separator 'Common options:'
op.on('-h', '--help') do op.on('-h', '--help') do
...@@ -26,7 +21,10 @@ class GeoLogCursorOptionParser ...@@ -26,7 +21,10 @@ class GeoLogCursorOptionParser
exit exit
end end
op.on('-v', '--version') do op.on('-v', '--version') do
puts version # Load only necessary libraries for faster startup
require "#{File.dirname(__FILE__)}/../lib/gitlab/geo/log_cursor/daemon"
puts Gitlab::Geo::LogCursor::Daemon::VERSION
exit exit
end end
op.separator '' op.separator ''
...@@ -40,5 +38,8 @@ end ...@@ -40,5 +38,8 @@ end
if $0 == __FILE__ if $0 == __FILE__
options = GeoLogCursorOptionParser.parse(ARGV) options = GeoLogCursorOptionParser.parse(ARGV)
# We load rails environment / initializers only here to get faster command line startup when `--help` and `--version`
require "#{File.dirname(__FILE__)}/../config/environment"
Gitlab::Geo::LogCursor::Daemon.new(options).run! Gitlab::Geo::LogCursor::Daemon.new(options).run!
end end
---
title: Reduce the number of Elasticsearch client instances that are created
merge_request: 3432
author:
type: fixed
---
title: 'Geo: Expire and resync attachments from renamed projects in secondary nodes
when using legacy storage'
merge_request: 3259
author:
type: added
---
title: Introduce EEU lincese with epics as the first feature
merge_request:
author:
type: added
---
title: Show SAST results in MR widget
merge_request: 3207
author:
type: added
---
title: Add ability to create new epics
merge_request:
author:
type: added
---
title: Prevent the Geo log cursor from running on primary nodes
merge_request: 3411
author:
type: fixed
---
title: Remove the full-scan option from the Geo log cursor
merge_request: 3412
author:
type: removed
---
title: Fix generated clone URLs for wikis on Geo secondaries
merge_request: 3448
author:
type: fixed
---
title: 'Fix: Geo API bug. Statistic is not collected when prometheus is disabled'
merge_request:
author:
type: fixed
---
title: Fix error when entering an invalid url to push to or pull from a remote repository
merge_request: 3389
author:
type: fixed
---
title: Geo - Ensure that repository deletions in a primary node are correctly deleted
in a secondary node
merge_request:
author:
type: fixed
---
title: Make GeoLogCursor Highly Available
merge_request: 3305
author:
type: added
---
title: Add warning when Geo is configured insecurely
merge_request: 3368
author:
type: added
---
title: Remove duplicate delete button in epic
merge_request:
author:
type: fixed
---
title: Clean up empty fork networks
merge_request: 15373
author:
type: other
---
title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits
per user
merge_request: 14708
author:
type: added
...@@ -130,7 +130,7 @@ module Gitlab ...@@ -130,7 +130,7 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb) config.action_view.sanitized_allowed_protocols = %w(smb)
config.middleware.insert_before Warden::Manager, Rack::Attack config.middleware.insert_after Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains # Allow access to GitLab API from other domains
config.middleware.insert_before Warden::Manager, Rack::Cors do config.middleware.insert_before Warden::Manager, Rack::Cors do
......
...@@ -459,15 +459,15 @@ ...@@ -459,15 +459,15 @@
:versions: [] :versions: []
:when: 2017-09-13 17:31:16.425819400 Z :when: 2017-09-13 17:31:16.425819400 Z
- - :approve - - :approve
- gitlab-svgs - "@gitlab-org/gitlab-svgs"
- :who: Tim Zallmann - :who: Tim Zallmann
:why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs :why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
:versions: [] :versions: []
:when: 2017-09-19 14:36:32.795496000 Z :when: 2017-09-19 14:36:32.795496000 Z
- - :license - - :license
- pikaday - pikaday
- MIT - MIT
- :who: - :who:
:why: :why:
:versions: [] :versions: []
:when: 2017-10-17 17:46:12.367554000 Z :when: 2017-10-17 17:46:12.367554000 Z
...@@ -5,20 +5,38 @@ require 'gitlab/current_settings' ...@@ -5,20 +5,38 @@ require 'gitlab/current_settings'
module Elasticsearch module Elasticsearch
module Model module Model
module Client module Client
# This mutex is only used to synchronize *creation* of a new client, so
# all including classes can share the same client instance
CLIENT_MUTEX = Mutex.new
cattr_accessor :cached_client
cattr_accessor :cached_config
module ClassMethods module ClassMethods
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
def client(client = nil) # Override the default ::Elasticsearch::Model::Client implementation to
if @client.nil? || es_configuration_changed? # return a client configured from application settings. All including
@es_config = current_application_settings.elasticsearch_config # classes will use the same instance, which is refreshed automatically
@client = ::Gitlab::Elastic::Client.build(@es_config) # if the settings change.
end #
# _client is present to match the arity of the overridden method, where
# it is also not used.
#
# @return [Elasticsearch::Transport::Client]
def client(_client = nil)
store = ::Elasticsearch::Model::Client
@client store::CLIENT_MUTEX.synchronize do
end config = current_application_settings.elasticsearch_config
if store.cached_client.nil? || config != store.cached_config
store.cached_client = ::Gitlab::Elastic::Client.build(config)
store.cached_config = config
end
end
def es_configuration_changed? store.cached_client
@es_config != current_application_settings.elasticsearch_config
end end
end end
end end
......
...@@ -10,4 +10,30 @@ module Gollum ...@@ -10,4 +10,30 @@ module Gollum
index.send(name, *args) index.send(name, *args)
end end
end end
class Wiki
def pages(treeish = nil, limit: nil)
tree_list((treeish || @ref), limit: limit)
end
def tree_list(ref, limit: nil)
if (sha = @access.ref_to_sha(ref))
commit = @access.commit(sha)
tree_map_for(sha).inject([]) do |list, entry|
next list unless @page_class.valid_page_name?(entry.name)
list << entry.page(self, commit)
break list if limit && list.size >= limit
list
end
else
[]
end
end
end
end
Rails.application.configure do
config.after_initialize do
Gollum::Page.per_page = Kaminari.config.default_per_page
end
end end
module Gitlab::Throttle
def self.settings
Gitlab::CurrentSettings.current_application_settings
end
def self.unauthenticated_options
limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period }
period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds }
{ limit: limit_proc, period: period_proc }
end
def self.authenticated_api_options
limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period }
period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds }
{ limit: limit_proc, period: period_proc }
end
def self.authenticated_web_options
limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period }
period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds }
{ limit: limit_proc, period: period_proc }
end
end
class Rack::Attack
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
req.unauthenticated? &&
req.ip
end
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
req.api_request? &&
req.authenticated_user_id
end
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
req.web_request? &&
req.authenticated_user_id
end
class Request
def unauthenticated?
!authenticated_user_id
end
def authenticated_user_id
Gitlab::Auth::RequestAuthenticator.new(self).user&.id
end
def api_request?
path.start_with?('/api')
end
def web_request?
!api_request?
end
end
end
...@@ -35,6 +35,14 @@ resource :profile, only: [:show, :update] do ...@@ -35,6 +35,14 @@ resource :profile, only: [:show, :update] do
put :resend_confirmation_instructions put :resend_confirmation_instructions
end end
end end
## EE-specific
resource :slack, only: [:edit] do
member do
get :slack_link
end
end
resources :chat_names, only: [:index, :new, :create, :destroy] do resources :chat_names, only: [:index, :new, :create, :destroy] do
collection do collection do
delete :deny delete :deny
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const sourcePath = path.join('node_modules', 'gitlab-svgs', 'dist'); const svgsPackageName = '@gitlab-org/gitlab-svgs';
const sourcePathIllustrations = path.join('node_modules', 'gitlab-svgs', 'dist', 'illustrations'); const sourcePath = path.join('node_modules', svgsPackageName, 'dist');
const sourcePathIllustrations = path.join('node_modules', svgsPackageName, 'dist', 'illustrations');
const destPath = path.normalize(path.join('app', 'assets', 'images')); const destPath = path.normalize(path.join('app', 'assets', 'images'));
// Actual Task copying the 2 files + all illustrations // Actual Task copying the 2 files + all illustrations
......
...@@ -27,6 +27,7 @@ var config = { ...@@ -27,6 +27,7 @@ var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'), context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: { entry: {
account: './profile/account/index.js', account: './profile/account/index.js',
add_gitlab_slack_application: './add_gitlab_slack_application/index.js',
balsamiq_viewer: './blob/balsamiq_viewer.js', balsamiq_viewer: './blob/balsamiq_viewer.js',
blob: './blob_edit/blob_bundle.js', blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js', boards: './boards/boards_bundle.js',
...@@ -41,6 +42,7 @@ var config = { ...@@ -41,6 +42,7 @@ var config = {
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js', epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js', graphs: './graphs/graphs_bundle.js',
graphs_charts: './graphs/graphs_charts.js', graphs_charts: './graphs/graphs_charts.js',
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddGlobalRateLimitsToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :application_settings, :throttle_unauthenticated_enabled, :boolean, default: false, allow_null: false
add_column_with_default :application_settings, :throttle_unauthenticated_requests_per_period, :integer, default: 3600, allow_null: false
add_column_with_default :application_settings, :throttle_unauthenticated_period_in_seconds, :integer, default: 3600, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_api_enabled, :boolean, default: false, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_api_requests_per_period, :integer, default: 7200, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_api_period_in_seconds, :integer, default: 3600, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_web_enabled, :boolean, default: false, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_web_requests_per_period, :integer, default: 7200, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_web_period_in_seconds, :integer, default: 3600, allow_null: false
end
def down
remove_column :application_settings, :throttle_authenticated_web_period_in_seconds
remove_column :application_settings, :throttle_authenticated_web_requests_per_period
remove_column :application_settings, :throttle_authenticated_web_enabled
remove_column :application_settings, :throttle_authenticated_api_period_in_seconds
remove_column :application_settings, :throttle_authenticated_api_requests_per_period
remove_column :application_settings, :throttle_authenticated_api_enabled
remove_column :application_settings, :throttle_unauthenticated_period_in_seconds
remove_column :application_settings, :throttle_unauthenticated_requests_per_period
remove_column :application_settings, :throttle_unauthenticated_enabled
end
end
class RemoveEmptyForkNetworks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 10_000
class MigrationForkNetwork < ActiveRecord::Base
include EachBatch
self.table_name = 'fork_networks'
end
class MigrationForkNetworkMembers < ActiveRecord::Base
self.table_name = 'fork_network_members'
end
disable_ddl_transaction!
def up
say 'Deleting empty ForkNetworks in batches'
has_members = MigrationForkNetworkMembers
.where('fork_network_members.fork_network_id = fork_networks.id')
.select(1)
MigrationForkNetwork.where('NOT EXISTS (?)', has_members)
.each_batch(of: BATCH_SIZE) do |networks|
deleted = networks.delete_all
say "Deleted #{deleted} rows in batch"
end
end
def down
# nothing
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: 20171107144726) do ActiveRecord::Schema.define(version: 20171114104051) 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"
...@@ -164,6 +164,15 @@ ActiveRecord::Schema.define(version: 20171107144726) do ...@@ -164,6 +164,15 @@ ActiveRecord::Schema.define(version: 20171107144726) do
t.boolean "remote_mirror_available", default: true, null: false t.boolean "remote_mirror_available", default: true, null: false
t.integer "circuitbreaker_access_retries", default: 3 t.integer "circuitbreaker_access_retries", default: 3
t.integer "circuitbreaker_backoff_threshold", default: 80 t.integer "circuitbreaker_backoff_threshold", default: 80
t.boolean "throttle_unauthenticated_enabled", default: false, null: false
t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false
t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false
t.boolean "throttle_authenticated_api_enabled", default: false, null: false
t.integer "throttle_authenticated_api_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_api_period_in_seconds", default: 3600, null: false
t.boolean "throttle_authenticated_web_enabled", default: false, null: false
t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
......
...@@ -66,7 +66,7 @@ the communication as well as information required for running the service. ...@@ -66,7 +66,7 @@ the communication as well as information required for running the service.
Bellow you will find details on each service and the minimum required Bellow you will find details on each service and the minimum required
information you need to provide. information you need to provide.
#### Consul #### Consul information
When using default setup, minimum configuration requires: When using default setup, minimum configuration requires:
...@@ -77,8 +77,7 @@ Can be generated with: ...@@ -77,8 +77,7 @@ Can be generated with:
```sh ```sh
echo -n 'CONSUL_DATABASE_PASSWORDCONSUL_USERNAME' | md5sum echo -n 'CONSUL_DATABASE_PASSWORDCONSUL_USERNAME' | md5sum
``` ```
- You'll also need to supply the IP addresses or DNS records of Consul - `CONSUL_SERVER_NODES`. The IP addresses or DNS records of the Consul server nodes.
server nodes.
Few notes on the service itself: Few notes on the service itself:
...@@ -92,7 +91,7 @@ database ...@@ -92,7 +91,7 @@ database
- `/var/opt/gitlab/pgbouncer/pg_auth`: hashed - `/var/opt/gitlab/pgbouncer/pg_auth`: hashed
- `/var/opt/gitlab/gitlab-consul/.pgpass`: plaintext - `/var/opt/gitlab/gitlab-consul/.pgpass`: plaintext
#### PostgreSQL #### PostgreSQL information
When configuring PostgreSQL, we will set `max_wal_senders` to one more than When configuring PostgreSQL, we will set `max_wal_senders` to one more than
the number of database nodes in the cluster. the number of database nodes in the cluster.
...@@ -113,10 +112,15 @@ server nodes. ...@@ -113,10 +112,15 @@ server nodes.
We will need the following password information for the application's database user: We will need the following password information for the application's database user:
- `POSTGRESQL_USERNAME`. Defaults to `gitlab`
- `POSTGRESQL_USER_PASSWORD`. The password for the database user - `POSTGRESQL_USER_PASSWORD`. The password for the database user
- `POSTGRESQL_PASSWORD_HASH`. The md5 hash of POSTGRESQL_USER_PASSWORD - `POSTGRESQL_PASSWORD_HASH`. This is a hash generated out of the username/password pair.
Can be generated with:
```sh
echo -n 'POSTGRESQL_USER_PASSWORDPOSTGRESQL_USERNAME' | md5sum
```
#### Pgbouncer #### Pgbouncer information
When using default setup, minimum configuration requires: When using default setup, minimum configuration requires:
...@@ -140,7 +144,7 @@ Few notes on the service itself: ...@@ -140,7 +144,7 @@ Few notes on the service itself:
- `/etc/gitlab/gitlab.rb`: hashed, and in plain text - `/etc/gitlab/gitlab.rb`: hashed, and in plain text
- `/var/opt/gitlab/pgbouncer/pg_auth`: hashed - `/var/opt/gitlab/pgbouncer/pg_auth`: hashed
#### Repmgr #### Repmgr information
When using default setup, you will only have to prepare the network subnets that will When using default setup, you will only have to prepare the network subnets that will
be allowed to authenticate with the service. be allowed to authenticate with the service.
...@@ -165,7 +169,7 @@ When installing the GitLab package, do not supply `EXTERNAL_URL` value. ...@@ -165,7 +169,7 @@ When installing the GitLab package, do not supply `EXTERNAL_URL` value.
Each node needs to be configured to run only the services it needs. Each node needs to be configured to run only the services it needs.
### Consul nodes ### Configuring the Consul nodes
On each Consul node perform the following: On each Consul node perform the following:
...@@ -192,7 +196,7 @@ See `START user configuration` section in the next step for required information ...@@ -192,7 +196,7 @@ See `START user configuration` section in the next step for required information
# Replace placeholders: # Replace placeholders:
# #
# Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
# with real information. # with the addresses gathered for CONSUL_SERVER_NODES
consul['configuration'] = { consul['configuration'] = {
server: true, server: true,
retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z) retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
...@@ -205,7 +209,7 @@ See `START user configuration` section in the next step for required information ...@@ -205,7 +209,7 @@ See `START user configuration` section in the next step for required information
After this is completed on each Consul server node, proceed further. After this is completed on each Consul server node, proceed further.
### Database nodes ### Configuring the Database nodes
On each database node perform the following: On each database node perform the following:
...@@ -257,7 +261,7 @@ See `START user configuration` section in the next step for required information ...@@ -257,7 +261,7 @@ See `START user configuration` section in the next step for required information
# Replace placeholders: # Replace placeholders:
# #
# Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
# with real information. # with the addresses gathered for CONSUL_SERVER_NODES
consul['configuration'] = { consul['configuration'] = {
retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z) retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
} }
...@@ -317,7 +321,7 @@ your configuration ...@@ -317,7 +321,7 @@ your configuration
# Replace placeholders: # Replace placeholders:
# #
# Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
# with real information. # with the addresses gathered for CONSUL_SERVER_NODES
consul['configuration'] = { consul['configuration'] = {
retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z) retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
} }
...@@ -351,7 +355,7 @@ attributes set, but the following need to be set. ...@@ -351,7 +355,7 @@ attributes set, but the following need to be set.
After reconfigure successfully runs, the following steps must be completed to After reconfigure successfully runs, the following steps must be completed to
get the cluster up and running. get the cluster up and running.
### Consul ### Consul nodes post-configuration
Verify the nodes are all communicating: Verify the nodes are all communicating:
...@@ -372,7 +376,7 @@ DATABASE_NODE_THREE XXX.XXX.XXX.YYY:8301 alive client 0.9.2 2 gitl ...@@ -372,7 +376,7 @@ DATABASE_NODE_THREE XXX.XXX.XXX.YYY:8301 alive client 0.9.2 2 gitl
PGBOUNCER_NODE XXX.XXX.XXX.YYY:8301 alive client 0.9.0 2 gitlab_consul PGBOUNCER_NODE XXX.XXX.XXX.YYY:8301 alive client 0.9.0 2 gitlab_consul
``` ```
### Database nodes ### Database nodes post-configuration
#### Primary node #### Primary node
...@@ -434,13 +438,13 @@ as `MASTER_NODE_NAME`. ...@@ -434,13 +438,13 @@ as `MASTER_NODE_NAME`.
Repeat the above steps on all secondary nodes. Repeat the above steps on all secondary nodes.
### Pgbouncer node ### Pgbouncer node post-configuration
1. Create a `.pgpass` file user for the `CONSUL_USER` account to be able to 1. Create a `.pgpass` file user for the `CONSUL_USER` account to be able to
reload pgbouncer. Confirm `PGBOUNCER_PASSWORD` twice when asked: reload pgbouncer. Confirm `PGBOUNCER_PASSWORD` twice when asked:
```sh ```sh
gitlab-ctl write-pgpass --host PGBOUNCER_HOST --database pgbouncer --user pgbouncer --hostuser gitlab-consul gitlab-ctl write-pgpass --host 127.0.0.1 --database pgbouncer --user pgbouncer --hostuser gitlab-consul
``` ```
1. Ensure the node is talking to the current master: 1. Ensure the node is talking to the current master:
...@@ -471,7 +475,7 @@ Repeat the above steps on all secondary nodes. ...@@ -471,7 +475,7 @@ Repeat the above steps on all secondary nodes.
(2 rows) (2 rows)
``` ```
### Application node ### Application node post-configuration
Ensure that all migrations ran: Ensure that all migrations ran:
......
...@@ -42,10 +42,6 @@ GitLab does not recommend using EFS with GitLab. ...@@ -42,10 +42,6 @@ GitLab does not recommend using EFS with GitLab.
are allocated. For smaller volumes, users may experience decent performance are allocated. For smaller volumes, users may experience decent performance
for a period of time due to 'Burst Credits'. Over a period of weeks to months for a period of time due to 'Burst Credits'. Over a period of weeks to months
credits may run out and performance will bottom out. credits may run out and performance will bottom out.
- To keep "Burst Credits" available, it may be necessary to provision more space
with 'dummy data'. However, this may get expensive.
- Another option to maintain "Burst Credits" is to use FS Cache on the server so
that AWS doesn't always have to go into EFS to access files.
- For larger volumes, allocated IOPS may not be the problem. Workloads where - For larger volumes, allocated IOPS may not be the problem. Workloads where
many small files are written in a serialized manner are not well-suited for EFS. many small files are written in a serialized manner are not well-suited for EFS.
EBS with an NFS server on top will perform much better. EBS with an NFS server on top will perform much better.
......
...@@ -180,9 +180,14 @@ Read how to [update your Geo nodes to the latest GitLab version](updating_the_ge ...@@ -180,9 +180,14 @@ Read how to [update your Geo nodes to the latest GitLab version](updating_the_ge
## Current limitations ## Current limitations
- You cannot push code to secondary nodes - You cannot push code to secondary nodes
- The primary node has to be online for OAuth login to happen (existing sessions and Git are not affected) - The primary node has to be online for OAuth login to happen (existing
- It works for repos, wikis, issues, and merge requests, but not for job logs, sessions and Git are not affected)
artifacts, and Docker images of the Container Registry - It works for repos, wikis, issues, and merge requests
- It does not work for job logs, artifacts, Docker images of the Container
Registry, and GitLab Pages
- It does not work for attachments uploaded before GitLab 9.0 because they will
not present in the uploads table, until
[#29249](https://gitlab.com/gitlab-org/gitlab-ce/issues/29240) is fixed.
## Frequently Asked Questions ## Frequently Asked Questions
......
...@@ -16,7 +16,7 @@ Before attempting the steps in this stage, complete all prior stages. ...@@ -16,7 +16,7 @@ Before attempting the steps in this stage, complete all prior stages.
1. [Setup the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology). 1. [Setup the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure SSH authorizations to use the database](ssh.md) 1. [Configure SSH authorizations to use the database](ssh.md)
1. **Configure GitLab to set the primary and secondary nodes.** 1. **Configure GitLab to set the primary and secondary nodes.**
1. Optional: [Configure a secondary LDAP server](../administration/auth/ldap.md) for the secondary. See [notes on LDAP](#ldap). 1. Optional: [Configure a secondary LDAP server](../administration/auth/ldap.md) for the secondary.
1. [Follow the after setup steps](after_setup.md). 1. [Follow the after setup steps](after_setup.md).
[install-ee]: https://about.gitlab.com/downloads-ee/ "GitLab Enterprise Edition Omnibus packages downloads page" [install-ee]: https://about.gitlab.com/downloads-ee/ "GitLab Enterprise Edition Omnibus packages downloads page"
...@@ -38,16 +38,14 @@ After having installed GitLab Enterprise Edition in the instance that will serve ...@@ -38,16 +38,14 @@ After having installed GitLab Enterprise Edition in the instance that will serve
as a Geo node and set up the [database replication](database.md), the next steps as a Geo node and set up the [database replication](database.md), the next steps
can be summed up to: can be summed up to:
1. Configure the primary node
1. Replicate some required configurations between the primary and the secondaries 1. Replicate some required configurations between the primary and the secondaries
1. Configure a second, tracking database on each secondary 1. Configure a second, tracking database on each secondary
1. Configure every secondary node in the primary's Admin screen
1. Start GitLab on the secondary node's machine 1. Start GitLab on the secondary node's machine
### Prerequisites ### Prerequisites
This is the last step of configuring a Geo node. Make sure you have followed the This is the last step of configuring a Geo secondary node. Make sure you have
first two steps of the [Setup instructions](README.md#setup-instructions): followed the first two steps of the [Setup instructions](README.md#setup-instructions):
1. You have already installed on the secondary server the same version of 1. You have already installed on the secondary server the same version of
GitLab Enterprise Edition that is present on the primary server. GitLab Enterprise Edition that is present on the primary server.
...@@ -57,31 +55,8 @@ first two steps of the [Setup instructions](README.md#setup-instructions): ...@@ -57,31 +55,8 @@ first two steps of the [Setup instructions](README.md#setup-instructions):
1. Your nodes must have an NTP service running to synchronize the clocks. 1. Your nodes must have an NTP service running to synchronize the clocks.
You can use different timezones, but the hour relative to UTC can't be more You can use different timezones, but the hour relative to UTC can't be more
than 60 seconds off from each node. than 60 seconds off from each node.
1. You have set up another PostgreSQL database that can store writes for the secondary.
Note that this MUST be on another instance, since the primary replicated database
is read-only.
Some of the following steps require to configure the primary and secondary ### Step 1. Copying the database encryption key
nodes almost at the same time. For your convenience make sure you have SSH
logins opened on all nodes as we will be moving back and forth.
### Step 1. Adding the primary GitLab node
1. SSH into the **primary** node and login as root:
```
sudo -i
```
1. Execute the command below to define the node as primary Geo node:
```
gitlab-ctl set-geo-primary-node
```
This command will use your defined `external_url` in `gitlab.rb`
### Step 2. Copying the database encryption key
GitLab stores a unique encryption key in disk that we use to safely store GitLab stores a unique encryption key in disk that we use to safely store
sensitive data in the database. Any secondary node must have the sensitive data in the database. Any secondary node must have the
...@@ -105,16 +80,15 @@ sensitive data in the database. Any secondary node must have the ...@@ -105,16 +80,15 @@ sensitive data in the database. Any secondary node must have the
sudo -i sudo -i
``` ```
1. Open the secrets file and paste the value of `db_key_base` you copied in the 1. Add the following to /etc/gitlab/gitlab.rb, replacing `encryption-key` with the output
previous step: of the previous command:
``` ```ruby
editor /etc/gitlab/gitlab-secrets.json gitlab_rails['db_key_base'] = "encryption-key"
``` ```
1. Save and close the file. 1. Reconfigure the secondary node for the change to take effect:
1. Reconfigure for the change to take effect.
``` ```
gitlab-ctl reconfigure gitlab-ctl reconfigure
``` ```
...@@ -125,7 +99,7 @@ Meanwhile, the primary node will start to notify changes to the secondary, which ...@@ -125,7 +99,7 @@ Meanwhile, the primary node will start to notify changes to the secondary, which
will act on those notifications immediately. Make sure the secondary instance is will act on those notifications immediately. Make sure the secondary instance is
running and accessible. running and accessible.
### Step 3. Enabling hashed storage (from GitLab 10.0) ### Step 2. Enabling hashed storage (from GitLab 10.0)
1. Visit the **primary** node's **Admin Area ➔ Settings** 1. Visit the **primary** node's **Admin Area ➔ Settings**
(`/admin/application_settings`) in your browser (`/admin/application_settings`) in your browser
...@@ -137,7 +111,7 @@ Using hashed storage significantly improves Geo replication - project and group ...@@ -137,7 +111,7 @@ Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes - so we recommend it is renames no longer require synchronization between nodes - so we recommend it is
used for all GitLab Geo installations. used for all GitLab Geo installations.
### Step 4. (Optional) Configuring the secondary to trust the primary ### Step 3. (Optional) Configuring the secondary to trust the primary
You can safely skip this step if your primary uses a CA-issued HTTPS certificate. You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
...@@ -147,7 +121,7 @@ certificate from the primary and follow ...@@ -147,7 +121,7 @@ certificate from the primary and follow
[these instructions](https://docs.gitlab.com/omnibus/settings/ssl.html) [these instructions](https://docs.gitlab.com/omnibus/settings/ssl.html)
on the secondary. on the secondary.
### Step 5. Managing the secondary GitLab node ### Step 4. Managing the secondary GitLab node
You can monitor the status of the syncing process on a secondary node You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
...@@ -204,17 +178,12 @@ secondary nodes, but repositories that have not been selected will be empty. ...@@ -204,17 +178,12 @@ secondary nodes, but repositories that have not been selected will be empty.
1. Secondary nodes won't pull repositories that do not belong to the selected 1. Secondary nodes won't pull repositories that do not belong to the selected
groups to be replicated. groups to be replicated.
## Adding another secondary Geo node
To add another Geo node in an already Geo configured infrastructure, just follow
[the steps starting from step 2](#step-2-copying-the-database-encryption-key)
Just omit the first step that sets up the primary node.
## Replicating wikis and repositories over SSH ## Replicating wikis and repositories over SSH
>**Warning:**
In GitLab 10.2, replicating repositories and wikis over SSH was deprecated. In GitLab 10.2, replicating repositories and wikis over SSH was deprecated.
Support for this option will be removed within a few releases, but if you need Support for SSH replication will be removed in 10.3. These instructions should
to add a new secondary in the short term, you can follow these instructions: only be used if you need to add a new secondary in the short term.
1. SSH into the **secondary** node and login as root: 1. SSH into the **secondary** node and login as root:
...@@ -224,9 +193,9 @@ to add a new secondary in the short term, you can follow these instructions: ...@@ -224,9 +193,9 @@ to add a new secondary in the short term, you can follow these instructions:
1. Add the primary's SSH key fingerprint to the `known_hosts` file. 1. Add the primary's SSH key fingerprint to the `known_hosts` file.
```bash ```bash
sudo -u git -H ssh git@<primary-node-url> sudo -u git -H ssh git@<primary-node-url>
``` ```
Replace `<primary-node-url>` with the FQDN of the primary node. You should Replace `<primary-node-url>` with the FQDN of the primary node. You should
manually check the displayed fingerprint against a trusted record of the manually check the displayed fingerprint against a trusted record of the
...@@ -241,7 +210,7 @@ to add a new secondary in the short term, you can follow these instructions: ...@@ -241,7 +210,7 @@ to add a new secondary in the short term, you can follow these instructions:
``` ```
Follow the steps above to set up the new Geo node. When you reach Follow the steps above to set up the new Geo node. When you reach
[Step 5: Enabling the secondary GitLab node](#step-5-managing-the-secondary-gitlab-node) [Step 4: Enabling the secondary GitLab node](#step-4-managing-the-secondary-gitlab-node)
select "SSH (deprecated)" instead of "HTTP/HTTPS", and populate the "Public Key" select "SSH (deprecated)" instead of "HTTP/HTTPS", and populate the "Public Key"
with the output of the previous command (beginning `ssh-rsa AAAA...`). with the output of the previous command (beginning `ssh-rsa AAAA...`).
......
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.
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.
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.
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.
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.
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.
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.
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