Commit a2479aaa authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 38869-datetime

* master: (82 commits)
  Docs: add EEU tier to the landing page
  CE backport of ProtectedBranches API changes
  Support uploads for groups
  Update pipeline create chain Prometheus metric
  Don't set timeago title to what was already there.
  Resolve "Display member role per project"
  The API isn't using the appropriate services for managing forks
  Add chevron to create dropdown on repository page
  Rename GKE as Kubernetes Engine
  Fix specs for MySQL
  Fix broken tests
  Refactor banzai to support referencing from group context
  Fix specs after rebase
  Prevent dups when using StringIO for binary reads
  Bump redis-rails to 5.0.2 to get redis-store security updates
  add note on deploying Pages to a private network
  Bump GITLAB_SHELL_VERSION
  Updates the dropdown to match the docs and remove old hack of stop event propagation
  Move invalid builds counter out of the transaction
  Add invalid builds counter metric to stage seeds class
  ...
parents 139ce1c4 17542a78
...@@ -171,7 +171,7 @@ gem 're2', '~> 1.1.1' ...@@ -171,7 +171,7 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# Cache # Cache
gem 'redis-rails', '~> 5.0.1' gem 'redis-rails', '~> 5.0.2'
# Redis # Redis
gem 'redis', '~> 3.2' gem 'redis', '~> 3.2'
......
...@@ -699,24 +699,24 @@ GEM ...@@ -699,24 +699,24 @@ GEM
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.3) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0) redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.1) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (1.6.0) redis-rack (2.0.3)
rack (~> 1.5) rack (>= 1.5, < 3)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-rails (5.0.1) redis-rails (5.0.2)
redis-actionpack (~> 5.0.0) redis-actionpack (>= 5.0, < 6)
redis-activesupport (~> 5.0.0) redis-activesupport (>= 5.0, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-store (1.2.0) redis-store (1.4.1)
redis (>= 2.2) redis (>= 2.2, < 5)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
...@@ -1130,7 +1130,7 @@ DEPENDENCIES ...@@ -1130,7 +1130,7 @@ DEPENDENCIES
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
......
{"iconCount":179,"spriteSize":81882,"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","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","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-square","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":180,"spriteSize":82176,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","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-square","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 height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><g fill="#fee1d3"><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28z"/><rect height="30" rx="5" width="30" y="49"/></g><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="2" y="51"/><rect fill="#c3b8e3" height="50" rx="10" width="50" x="46" y="39"/><rect height="46" rx="10" stroke="#6b4fbb" stroke-width="4" width="46" x="48" y="41"/><rect fill="#fef0e8" height="30" rx="5" width="30" x="84"/><rect height="26" rx="5" stroke="#fee1d3" stroke-width="4" width="26" x="86" y="2"/><rect fill="#fee1d3" height="30" rx="5" width="30" x="84" y="98"/><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="86" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="112" y="49"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="114" y="51"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28" y="98"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="2"/></g></svg> <svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28zM3 52h24v24H3z" fill="#fee1d3"/><path d="M31 3h24v24H31z" fill="#f0edf8"/><path d="M87 3h24v24H87z" fill="#fef0e8"/><path d="M115 52h24v24h-24z" fill="#f0edf8"/><path d="M87 101h24v24H87z" fill="#fee1d3"/><path d="M31 101h24v24H31z" fill="#f0edf8"/><path d="M49 42h44v44H49z" fill="#c3b8e3"/><g fill-rule="nonzero"><path d="M5 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H5a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M56 43a6 6 0 0 0-6 6v30a6 6 0 0 0 6 6h30a6 6 0 0 0 6-6V49a6 6 0 0 0-6-6zm0-4h30c5.523 0 10 4.477 10 10v30c0 5.523-4.477 10-10 10H56c-5.523 0-10-4.477-10-10V49c0-5.523 4.477-10 10-10z" fill="#6b4fbb"/><path d="M89 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#fee1d3"/><path d="M89 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M117 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5h-20a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5zM33 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5zM33 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#e1dbf1"/></g></g></svg>
\ No newline at end of file \ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 374 268" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="d" width="230" height="176" rx="10" fill="#fff"/><rect id="e" width="14" rx="2" height="4"/><rect id="f" width="14" x="40" rx="2" height="4"/><rect id="g" width="14" x="40" y="24" rx="2" height="4"/><rect id="h" width="7" x="20" y="12" rx="2" height="4"/><rect id="i" width="7" y="24" rx="2" height="4"/><rect id="j" width="7" x="33" y="12" rx="2" height="4"/><circle id="l" cx="31" cy="31" r="31"/><circle id="c" cx="35" cy="35" r="35"/><circle id="a" cx="44" cy="44" r="44"/><circle id="b" cx="31" cy="31" r="31"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 94)"><circle cx="57" cy="57" r="44" fill="#f9f9f9"/><g transform="rotate(-7.999 120.507 -22.508)"><use fill="#fff" xlink:href="#a"/><circle cx="44" cy="44" r="42" stroke="#eee" stroke-width="4"/><path fill="#fee1d3" fill-rule="nonzero" d="M34.394 55.736A4 4 0 0 1 36.706 55H56a6 6 0 0 0 6-6V35a6 6 0 0 0-6-6H34a6 6 0 0 0-6 6v25.26l6.394-4.529m2.312 3.264l-7.972 5.647A3.001 3.001 0 0 1 24 62.194v-27.2c0-5.523 4.477-10 10-10h22c5.523 0 10 4.477 10 10v14c0 5.523-4.477 10-10 10H36.706"/><path fill="#fc6d26" d="M38 40a2 2 0 1 1 .001 3.999A2 2 0 0 1 38 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 45 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 52 40"/></g></g><g transform="translate(48)"><circle cx="41" cy="41" r="31" fill="#f9f9f9"/><g transform="rotate(-7.999 84.554 -15.551)"><use fill="#fff" xlink:href="#b"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><rect width="20" height="4" x="21" y="29" fill="#6b4fbb" rx="2"/></g></g><path fill="#f9f9f9" d="M235.58 229H102c-6.627 0-12-5.373-12-12V65c0-6.627 5.373-12 12-12h206c6.627 0 12 5.373 12 12v18.399A34.834 34.834 0 0 1 337 79c19.33 0 35 15.67 35 35s-15.67 35-35 35a34.831 34.831 0 0 1-17-4.399v72.4c0 6.627-5.373 12-12 12h-11.58c.381 1.941.58 3.947.58 6 0 17.12-13.879 31-31 31-17.12 0-31-13.879-31-31 0-2.053.2-4.059.58-6"/><g transform="translate(87 50)"><g transform="rotate(7.999 -44.933 1563.894)"><use fill="#fff" xlink:href="#c"/><circle cx="35" cy="35" r="33" stroke="#eee" stroke-width="4"/><g transform="translate(20 19)"><circle cx="15" cy="16" r="15" fill="#f4f1fa" stroke="#6b4fbb" stroke-width="3"/><g fill="#6b4fbb"><path d="M19.419 6.996h-.007L16.959 4l-2.454 2.997H14.5L12.046 4 9.591 6.998h-.003L7.133 4 4.677 6.999H2.001c2.605-4.204 7.231-7 12.502-7 5.269 0 9.892 2.793 12.498 6.994h-2.676l-2.452-2.994-2.453 2.996"/><circle cx="9.5" cy="17.5" r="1.5"/><circle cx="20.5" cy="17.5" r="1.5"/></g></g></g><use xlink:href="#d"/><rect width="226" height="172" x="2" y="2" stroke="#eee" stroke-width="4" rx="10"/><rect width="4" height="122" x="33" y="42" fill="#eee" rx="2"/><g transform="translate(13 59)"><rect width="10" height="4" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="12" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="24" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="36" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="10" height="4" y="60" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="72" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="84" fill="#fee1d3" rx="2"/></g><g transform="translate(55 59)"><use fill="#6b4fbb" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fef0e9" rx="2"/><use fill="#fee1d3" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#e1dbf1" rx="2"/><rect width="21" height="4" y="36" fill="#eee" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/><g transform="translate(98)"><use fill="#fee1d3" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fc6d26" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#fef0e9" rx="2" id="k"/><use fill="#e1dbf1" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#f0edf8" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fc6d26" rx="2"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fee1d3" rx="2"/><rect width="21" height="4" y="36" fill="#fef0e9" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/></g><g transform="translate(0 60)"><use fill="#f0edf8" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#6b4fbb" rx="2"/><use fill="#e1dbf1" xlink:href="#f"/><use xlink:href="#k"/><use fill="#fee1d3" xlink:href="#g"/><use fill="#eee" xlink:href="#h"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fef0e9" rx="2"/><use fill="#fc6d26" xlink:href="#j"/></g><rect width="4" height="63" x="74" y="13" fill="#eee" rx="2"/></g><rect width="230" height="4" y="27" fill="#eee" rx="2"/></g><g transform="rotate(7.999 -1289.786 1797.583)"><use fill="#fff" xlink:href="#l"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><path fill="#fc6d26" d="M29 29h-6a2 2 0 1 0 0 4h6v6a2 2 0 1 0 4 0v-6h6a2 2 0 1 0 0-4h-6v-6a2 2 0 1 0-4 0v6"/></g></g></svg>
\ No newline at end of file
...@@ -287,6 +287,10 @@ class GfmAutoComplete { ...@@ -287,6 +287,10 @@ class GfmAutoComplete {
} }
setupLabels($input) { setupLabels($input) {
const fetchData = this.fetchData.bind(this);
const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
let command = '';
$input.atwho({ $input.atwho({
at: '~', at: '~',
alias: 'labels', alias: 'labels',
...@@ -309,8 +313,45 @@ class GfmAutoComplete { ...@@ -309,8 +313,45 @@ class GfmAutoComplete {
title: sanitize(m.title), title: sanitize(m.title),
color: m.color, color: m.color,
search: m.title, search: m.title,
set: m.set,
})); }));
}, },
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
command = subtextNodes.find((node) => {
if (node === LABEL_COMMAND.LABEL ||
node === LABEL_COMMAND.RELABEL ||
node === LABEL_COMMAND.UNLABEL) { return node; }
return null;
});
return match && match.length ? match[1] : null;
},
filter(query, data, searchKey) {
if (GfmAutoComplete.isLoading(data)) {
fetchData(this.$inputor, this.at);
return data;
}
if (data === GfmAutoComplete.defaultLoadingData) {
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
}
// The `LABEL_COMMAND.RELABEL` is intentionally skipped
// because we want to return all the labels (unfiltered) for that command.
if (command === LABEL_COMMAND.LABEL) {
// Return labels with set: undefined.
return data.filter(label => !label.set);
} else if (command === LABEL_COMMAND.UNLABEL) {
// Return labels with set: true.
return data.filter(label => label.set);
}
return data;
},
}, },
}); });
} }
...@@ -346,20 +387,7 @@ class GfmAutoComplete { ...@@ -346,20 +387,7 @@ class GfmAutoComplete {
return resultantValue; return resultantValue;
}, },
matcher(flag, subtext) { matcher(flag, subtext) {
// The below is taken from At.js source const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
const atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
const targetSubtext = subtext.split(/\s+/g).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
const match = regexp.exec(targetSubtext);
if (match) { if (match) {
return match[1]; return match[1];
...@@ -420,8 +448,27 @@ class GfmAutoComplete { ...@@ -420,8 +448,27 @@ class GfmAutoComplete {
return dataToInspect && return dataToInspect &&
(dataToInspect === loadingState || dataToInspect.name === loadingState); (dataToInspect === loadingState || dataToInspect.name === loadingState);
} }
static defaultMatcher(flag, subtext, controllers) {
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(controllers).join('|');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
return regexp.exec(targetSubtext);
}
} }
GfmAutoComplete.regexSubtext = new RegExp(/\s+/g);
GfmAutoComplete.defaultLoadingData = ['loading']; GfmAutoComplete.defaultLoadingData = ['loading'];
GfmAutoComplete.atTypeMap = { GfmAutoComplete.atTypeMap = {
......
...@@ -123,18 +123,23 @@ export default { ...@@ -123,18 +123,23 @@ export default {
:title="group.fullName" :title="group.fullName"
class="no-expand" class="no-expand"
data-placement="top" data-placement="top"
> >{{
{{group.name}} // ending bracket must be by closing tag to prevent
</a> // link hover text-decoration from over-extending
group.name
}}</a>
<span <span
v-if="group.permission" v-if="group.permission"
class="access-type" class="user-access-role"
> >
{{s__('GroupsTreeRole|as')}} {{group.permission}} {{group.permission}}
</span> </span>
</div> </div>
<div <div
class="description">{{group.description}}</div> v-if="group.description"
class="description">
{{group.description}}
</div>
</div> </div>
<group-folder <group-folder
v-if="group.isOpen && hasChildren" v-if="group.isOpen && hasChildren"
......
...@@ -93,8 +93,6 @@ export const renderTimeago = ($els) => { ...@@ -93,8 +93,6 @@ export const renderTimeago = ($els) => {
*/ */
export const localTimeAgo = ($timeagoEls, setTimeago = true) => { export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
$timeagoEls.each((i, el) => { $timeagoEls.each((i, el) => {
el.setAttribute('title', el.getAttribute('title'));
if (setTimeago) { if (setTimeago) {
// Recreate with custom template // Recreate with custom template
$(el).tooltip({ $(el).tooltip({
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
<div class="note-actions"> <div class="note-actions">
<span <span
v-if="accessLevel" v-if="accessLevel"
class="note-role note-role-access">{{accessLevel}}</span> class="note-role user-access-role">{{accessLevel}}</span>
<div <div
v-if="canAddAwardEmoji" v-if="canAddAwardEmoji"
class="note-actions-item"> class="note-actions-item">
......
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
components: { components: {
icon,
newModal, newModal,
upload, upload,
}, },
...@@ -41,11 +43,14 @@ ...@@ -41,11 +43,14 @@
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Create new file or directory" aria-label="Create new file or directory"
> >
<i <icon
class="fa fa-plus" name="plus"
aria-hidden="true" css-classes="pull-left"
> />
</i> <icon
name="arrow-down"
css-classes="pull-left"
/>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
......
...@@ -237,19 +237,6 @@ li.note { ...@@ -237,19 +237,6 @@ li.note {
} }
} }
.browser-alert {
padding: 10px;
text-align: center;
background: $error-bg;
color: $white-light;
font-weight: $gl-font-weight-bold;
a {
color: $white-light;
text-decoration: underline;
}
}
.warning_message { .warning_message {
border-left: 4px solid $warning-message-border; border-left: 4px solid $warning-message-border;
color: $warning-message-color; color: $warning-message-color;
......
...@@ -432,6 +432,7 @@ ...@@ -432,6 +432,7 @@
border-width: 1px; border-width: 1px;
width: 17px; width: 17px;
height: 17px; height: 17px;
top: 0;
} }
&:hover, &:hover,
......
...@@ -457,13 +457,11 @@ ul.indent-list { ...@@ -457,13 +457,11 @@ ul.indent-list {
ul.group-list-tree { ul.group-list-tree {
li.group-row { li.group-row {
&.has-description { &.has-description .title {
.title { line-height: inherit;
line-height: inherit;
}
} }
.title { &:not(.has-description) .title {
line-height: $list-text-height; line-height: $list-text-height;
} }
} }
......
...@@ -408,7 +408,6 @@ $location-icon-color: #e7e9ed; ...@@ -408,7 +408,6 @@ $location-icon-color: #e7e9ed;
* Notes * Notes
*/ */
$notes-light-color: $gl-text-color-secondary; $notes-light-color: $gl-text-color-secondary;
$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2; $note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0; $note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3; $note-targe3-inside: #ffffd3;
...@@ -557,6 +556,7 @@ $jq-ui-default-color: #777; ...@@ -557,6 +556,7 @@ $jq-ui-default-color: #777;
/* /*
* Label * Label
*/ */
$label-padding: 7px;
$label-gray-bg: #f8fafc; $label-gray-bg: #f8fafc;
$label-inverse-bg: #333; $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
......
...@@ -212,3 +212,15 @@ ...@@ -212,3 +212,15 @@
height: 50px; height: 50px;
} }
} }
.user-access-role {
display: inline-block;
color: $gl-text-color-secondary;
font-size: 12px;
line-height: 20px;
margin: -5px 3px;
padding: 0 $label-padding;
border: 1px solid $border-color;
border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal;
}
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
} }
.color-label { .color-label {
padding: 3px 7px; padding: 3px $label-padding;
border-radius: $label-border-radius; border-radius: $label-border-radius;
} }
......
...@@ -384,6 +384,12 @@ ...@@ -384,6 +384,12 @@
} }
} }
.nothing-here-block {
img {
width: 230px;
}
}
.mr-list { .mr-list {
.merge-request { .merge-request {
padding: 10px 0 10px 15px; padding: 10px 0 10px 15px;
......
...@@ -141,20 +141,20 @@ ...@@ -141,20 +141,20 @@
.sidebar-item-icon { .sidebar-item-icon {
border-radius: $border-radius-default; border-radius: $border-radius-default;
margin: 0 3px 0 -4px; margin: 0 5px 0 0;
vertical-align: middle; vertical-align: text-bottom;
&.is-active { &.is-active {
fill: $orange-600; fill: $orange-600;
} }
}
.sidebar-collapsed-icon .sidebar-item-icon { .sidebar-collapsed-icon & {
margin: 0; margin: 0;
} }
.sidebar-item-value .sidebar-item-icon { .sidebar-item-value & {
fill: $theme-gray-700; fill: $theme-gray-700;
}
} }
.sidebar-item-warning-message { .sidebar-item-warning-message {
......
...@@ -619,26 +619,17 @@ ul.notes { ...@@ -619,26 +619,17 @@ ul.notes {
} }
.note-role { .note-role {
margin: 0 3px;
}
.note-role-special {
position: relative; position: relative;
display: inline-block; display: inline-block;
color: $notes-role-color; color: $gl-text-color-secondary;
font-size: 12px; font-size: 12px;
line-height: 20px; text-shadow: 0 0 15px $gl-text-color-inverted;
margin: 0 3px;
&.note-role-access {
padding: 0 7px;
border: 1px solid $border-color;
border-radius: $label-border-radius;
}
&.note-role-special {
text-shadow: 0 0 15px $gl-text-color-inverted;
}
} }
/** /**
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
......
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
.nav-block { .nav-block {
margin: 10px 0; margin: 10px 0;
.btn .fa,
.btn svg {
color: $gl-text-color-secondary;
}
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: flex; display: flex;
...@@ -91,8 +96,12 @@ ...@@ -91,8 +96,12 @@
} }
.add-to-tree { .add-to-tree {
vertical-align: middle; vertical-align: top;
padding: 6px 10px; padding: 8px;
svg {
top: 0;
}
} }
.tree-table { .tree-table {
......
module RendersMemberAccess
def prepare_groups_for_rendering(groups)
preload_max_member_access_for_collection(Group, groups)
groups
end
def prepare_projects_for_rendering(projects)
preload_max_member_access_for_collection(Project, projects)
projects
end
private
def preload_max_member_access_for_collection(klass, collection)
return if !current_user || collection.blank?
method_name = "max_member_access_for_#{klass.name.underscore}_ids"
current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend
end
end
module UploadsActions module UploadsActions
include Gitlab::Utils::StrongMemoize
def create def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute link_to_file = UploadService.new(model, params[:file], uploader_class).execute
...@@ -24,4 +26,25 @@ module UploadsActions ...@@ -24,4 +26,25 @@ module UploadsActions
send_file uploader.file.path, disposition: disposition send_file uploader.file.path, disposition: disposition
end end
private
def uploader
strong_memoize(:uploader) do
return if show_model.nil?
file_uploader = FileUploader.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end
end end
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility include ParamsBackwardCompatibility
include RendersMemberAccess
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :default_sorting before_action :default_sorting
...@@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def load_projects(finder_params) def load_projects(finder_params)
ProjectsFinder projects = ProjectsFinder
.new(params: finder_params, current_user: current_user) .new(params: finder_params, current_user: current_user)
.execute .execute
.includes(:route, :creator, namespace: [:route, :owner]) .includes(:route, :creator, namespace: [:route, :owner])
prepare_projects_for_rendering(projects)
end end
def load_events def load_events
......
class Explore::ProjectsController < Explore::ApplicationController class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility include ParamsBackwardCompatibility
include RendersMemberAccess
before_action :set_non_archived_param before_action :set_non_archived_param
...@@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController
private private
def load_projects def load_projects
ProjectsFinder.new(current_user: current_user, params: params) projects = ProjectsFinder.new(current_user: current_user, params: params)
.execute .execute
.includes(:route, namespace: :route) .includes(:route, namespace: :route)
.page(params[:page]) .page(params[:page])
.without_count .without_count
prepare_projects_for_rendering(projects)
end end
end end
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
private
def show_model
strong_memoize(:show_model) do
group_id = params[:group_id]
Group.find_by_full_path(group_id)
end
end
def authorize_upload_file!
render_404 unless can?(current_user, :upload_file, group)
end
def uploader
strong_memoize(:uploader) do
file_uploader = uploader_class.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def uploader_class
NamespaceFileUploader
end
alias_method :model, :group
end
...@@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:members] before_action :load_autocomplete_service, except: [:members]
def members def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end end
def issues def issues
...@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end end
def labels def labels
render json: @autocomplete_service.labels render json: @autocomplete_service.labels(target)
end end
def milestones def milestones
...@@ -22,7 +22,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -22,7 +22,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end end
def commands def commands
render json: @autocomplete_service.commands(noteable, params[:type]) render json: @autocomplete_service.commands(target, params[:type])
end end
private private
...@@ -31,13 +31,13 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -31,13 +31,13 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
@autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user) @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
end end
def noteable def target
case params[:type] case params[:type]&.downcase
when 'Issue' when 'issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
when 'MergeRequest' when 'mergerequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
when 'Commit' when 'commit'
@project.commit(params[:type_id]) @project.commit(params[:type_id])
end end
end end
......
...@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController
private private
def uploader def show_model
return @uploader if defined?(@uploader) strong_memoize(:show_model) do
namespace = params[:namespace_id]
id = params[:project_id]
namespace = params[:namespace_id] Project.find_by_full_path("#{namespace}/#{id}")
id = params[:project_id]
file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
return
end end
@uploader = FileUploader.new(file_project, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
@uploader
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end end
alias_method :model, :project alias_method :model, :project
......
class UsersController < ApplicationController class UsersController < ApplicationController
include RoutableActions include RoutableActions
include RendersMemberAccess
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :user, except: [:exists] before_action :user, except: [:exists]
...@@ -116,14 +117,20 @@ class UsersController < ApplicationController ...@@ -116,14 +117,20 @@ class UsersController < ApplicationController
@projects = @projects =
PersonalProjectsFinder.new(user).execute(current_user) PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page]) .page(params[:page])
prepare_projects_for_rendering(@projects)
end end
def load_contributed_projects def load_contributed_projects
@contributed_projects = contributed_projects.joined(user) @contributed_projects = contributed_projects.joined(user)
prepare_projects_for_rendering(@contributed_projects)
end end
def load_groups def load_groups
@groups = JoinedGroupsFinder.new(user).execute(current_user) @groups = JoinedGroupsFinder.new(user).execute(current_user)
prepare_groups_for_rendering(@groups)
end end
def load_snippets def load_snippets
......
...@@ -86,6 +86,8 @@ module MarkupHelper ...@@ -86,6 +86,8 @@ module MarkupHelper
return '' unless text.present? return '' unless text.present?
context[:project] ||= @project context[:project] ||= @project
context[:group] ||= @group
html = markdown_unsafe(text, context) html = markdown_unsafe(text, context)
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
......
# Returns and caches in thread max member access for a resource
#
module BulkMemberAccessLoad
extend ActiveSupport::Concern
included do
# Determine the maximum access level for a group of resources in bulk.
#
# Returns a Hash mapping resource ID -> maximum access level.
def max_member_access_for_resource_ids(resource_klass, resource_ids, memoization_index = self.id, &block)
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
key = max_member_access_for_resource_key(resource_klass, memoization_index)
access = {}
if RequestStore.active?
RequestStore.store[key] ||= {}
access = RequestStore.store[key]
end
# Look up only the IDs we need
resource_ids = resource_ids - access.keys
return access if resource_ids.empty?
resource_access = yield(resource_ids)
access.merge!(resource_access)
missing_resource_ids = resource_ids - resource_access.keys
missing_resource_ids.each do |resource_id|
access[resource_id] = Gitlab::Access::NO_ACCESS
end
access
end
private
def max_member_access_for_resource_key(klass, memoization_index)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
end
end
end
# Placeholder class for model that is implemented in EE # Placeholder class for model that is implemented in EE
# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE # It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base class Epic < ActiveRecord::Base
# TODO: this will be implemented as part of #3853 def self.reference_prefix
def to_reference '&'
end
def self.reference_prefix_escaped
'&amp;'
end end
end end
...@@ -298,6 +298,10 @@ class Group < Namespace ...@@ -298,6 +298,10 @@ class Group < Namespace
end end
end end
def hashed_storage?(_feature)
false
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
class ProjectTeam class ProjectTeam
include BulkMemberAccessLoad
attr_accessor :project attr_accessor :project
def initialize(project) def initialize(project)
...@@ -157,39 +159,16 @@ class ProjectTeam ...@@ -157,39 +159,16 @@ class ProjectTeam
# #
# Returns a Hash mapping user ID -> maximum access level. # Returns a Hash mapping user ID -> maximum access level.
def max_member_access_for_user_ids(user_ids) def max_member_access_for_user_ids(user_ids)
user_ids = user_ids.uniq max_member_access_for_resource_ids(User, user_ids, project.id) do |user_ids|
key = "max_member_access:#{project.id}" project.project_authorizations
.where(user: user_ids)
access = {} .group(:user_id)
.maximum(:access_level)
if RequestStore.active?
RequestStore.store[key] ||= {}
access = RequestStore.store[key]
end end
# Look up only the IDs we need
user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations
.where(user: user_ids)
.group(:user_id)
.maximum(:access_level)
access.merge!(users_access)
missing_user_ids = user_ids - users_access.keys
missing_user_ids.each do |user_id|
access[user_id] = Gitlab::Access::NO_ACCESS
end
access
end end
def max_member_access(user_id) def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS max_member_access_for_user_ids([user_id])[user_id]
end end
private private
......
...@@ -17,6 +17,7 @@ class User < ActiveRecord::Base ...@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include FeatureGate include FeatureGate
include CreatedAtFilterable include CreatedAtFilterable
include IgnorableColumn include IgnorableColumn
include BulkMemberAccessLoad
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
...@@ -1144,6 +1145,34 @@ class User < ActiveRecord::Base ...@@ -1144,6 +1145,34 @@ class User < ActiveRecord::Base
super super
end end
# Determine the maximum access level for a group of projects in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_project_ids(project_ids)
max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
project_authorizations.where(project: project_ids)
.group(:project_id)
.maximum(:access_level)
end
end
def max_member_access_for_project(project_id)
max_member_access_for_project_ids([project_id])[project_id]
end
# Determine the maximum access level for a group of groups in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_group_ids(group_ids)
max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
end
end
def max_member_access_for_group(group_id)
max_member_access_for_group_ids([group_id])[group_id]
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -30,7 +30,12 @@ class GroupPolicy < BasePolicy ...@@ -30,7 +30,12 @@ class GroupPolicy < BasePolicy
rule { public_group } .enable :read_group rule { public_group } .enable :read_group
rule { logged_in_viewable }.enable :read_group rule { logged_in_viewable }.enable :read_group
rule { guest } .enable :read_group
rule { guest }.policy do
enable :read_group
enable :upload_file
end
rule { admin } .enable :read_group rule { admin } .enable :read_group
rule { has_projects } .enable :read_group rule { has_projects } .enable :read_group
......
...@@ -20,8 +20,23 @@ module Projects ...@@ -20,8 +20,23 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end end
def labels def labels(target = nil)
LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color]) labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title])
return labels unless target&.respond_to?(:labels)
issuable_label_titles = target.labels.pluck(:title)
if issuable_label_titles
labels = labels.as_json(only: [:title, :color])
issuable_label_titles.each do |issuable_label_title|
found_label = labels.find { |label| label['title'] == issuable_label_title }
found_label[:set] = true if found_label
end
end
labels
end end
def commands(noteable, type) def commands(noteable, type)
...@@ -33,7 +48,7 @@ module Projects ...@@ -33,7 +48,7 @@ module Projects
@project.merge_requests.build @project.merge_requests.build
end end
return [] unless noteable && noteable.is_a?(Issuable) return [] unless noteable&.is_a?(Issuable)
opts = { opts = {
project: project, project: project,
......
module Projects module Projects
class ForkService < BaseService class ForkService < BaseService
def execute def execute(fork_to_project = nil)
if fork_to_project
link_existing_project(fork_to_project)
else
fork_new_project
end
end
private
def link_existing_project(fork_to_project)
return if fork_to_project.forked?
link_fork_network(fork_to_project)
fork_to_project
end
def fork_new_project
new_params = { new_params = {
forked_from_project_id: @project.id, forked_from_project_id: @project.id,
visibility_level: allowed_visibility_level, visibility_level: allowed_visibility_level,
...@@ -21,15 +39,11 @@ module Projects ...@@ -21,15 +39,11 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
link_fork_network(new_project) link_fork_network(new_project)
new_project new_project
end end
private
def fork_network def fork_network
if @project.fork_network if @project.fork_network
@project.fork_network @project.fork_network
...@@ -43,9 +57,17 @@ module Projects ...@@ -43,9 +57,17 @@ module Projects
end end
end end
def link_fork_network(new_project) def link_fork_network(fork_to_project)
fork_network.fork_network_members.create(project: new_project, fork_network.fork_network_members.create(project: fork_to_project,
forked_from_project: @project) forked_from_project: @project)
# TODO: remove this when ForkedProjectLink model is removed
unless fork_to_project.forked_project_link
fork_to_project.create_forked_project_link(forked_to_project: fork_to_project,
forked_from_project: @project)
end
refresh_forks_count
end end
def refresh_forks_count def refresh_forks_count
......
module ProtectedBranches
class AccessLevelParams
attr_reader :type, :params
def initialize(type, params)
@type = type
@params = params_with_default(params)
end
def access_levels
ce_style_access_level
end
private
def params_with_default(params)
params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
params
end
def use_default_access_level?(params)
true
end
def ce_style_access_level
access_level = params[:"#{type}_access_level"]
return [] unless access_level
[{ access_level: access_level }]
end
end
end
module ProtectedBranches
class ApiService < BaseService
def create
@push_params = AccessLevelParams.new(:push, params)
@merge_params = AccessLevelParams.new(:merge, params)
verify_params!
protected_branch_params = {
name: params[:name],
push_access_levels_attributes: @push_params.access_levels,
merge_access_levels_attributes: @merge_params.access_levels
}
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
private
def verify_params!
# EE-only
end
end
end
...@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader ...@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader
# model - Object that responds to `full_path` and `disk_path` # model - Object that responds to `full_path` and `disk_path`
# #
# Returns a String without a trailing slash # Returns a String without a trailing slash
def self.dynamic_path_segment(project) def self.dynamic_path_segment(model)
if project.hashed_storage?(:attachments) if model.hashed_storage?(:attachments)
dynamic_path_builder(project.disk_path) dynamic_path_builder(model.disk_path)
else else
dynamic_path_builder(project.full_path) dynamic_path_builder(model.full_path)
end end
end end
......
class NamespaceFileUploader < FileUploader
def self.base_dir
File.join(root_dir, '-', 'system', 'namespace')
end
def self.dynamic_path_segment(model)
dynamic_path_builder(model.id.to_s)
end
private
def secure_url
File.join('/uploads', @secret, file.filename)
end
end
= render 'shared/projects/list', projects: @projects, ci: true = render 'shared/projects/list', projects: @projects, ci: true, user: current_user
= render 'shared/projects/list', projects: projects = render 'shared/projects/list', projects: projects, user: current_user
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}", members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_project_autocomplete_sources_path(project)}", issues: "#{issues_project_autocomplete_sources_path(project)}",
mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}", mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}",
labels: "#{labels_project_autocomplete_sources_path(project)}", labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
milestones: "#{milestones_project_autocomplete_sources_path(project)}", milestones: "#{milestones_project_autocomplete_sources_path(project)}",
commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}" commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}"
}; };
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- if defined?(nav) && nav - if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}" = render "layouts/nav/sidebar/#{nav}"
.content-wrapper.page-with-new-nav .content-wrapper.page-with-new-nav
= render 'shared/outdated_browser'
.mobile-overlay .mobile-overlay
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
......
...@@ -13,7 +13,8 @@ ...@@ -13,7 +13,8 @@
.location-badge= label .location-badge= label
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' }
%button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_content do = dropdown_content do
%ul %ul
......
...@@ -4,4 +4,10 @@ ...@@ -4,4 +4,10 @@
- nav "group" - nav "group"
- @left_sidebar = true - @left_sidebar = true
- content_for :page_specific_javascripts do
- if current_user
-# haml-lint:disable InlineJavaScript
:javascript
window.uploads_path = "#{group_uploads_path(@group)}";
= render template: "layouts/application" = render template: "layouts/application"
...@@ -75,5 +75,3 @@ ...@@ -75,5 +75,3 @@
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
= render 'shared/outdated_browser'
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
- if @cluster.managed? - if @cluster.managed?
.append-bottom-20 .append-bottom-20
%label.append-bottom-10 %label.append-bottom-10
= s_('ClusterIntegration|Google Container Engine') = s_('ClusterIntegration|Google Kubernetes Engine')
%p %p
- link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group .well.form-group
%label.text-danger %label.text-danger
= s_('ClusterIntegration|Remove cluster integration') = s_('ClusterIntegration|Remove cluster integration')
%p %p
= s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine.') = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.')
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"}) = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"})
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
.settings-content .settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
%p.js-error-reason %p.js-error-reason
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster is being created on Google Container Engine...') = s_('ClusterIntegration|Cluster is being created on Google Kubernetes Engine...')
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster\'s details') = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
%p %p
- if @cluster.enabled? - if @cluster.enabled?
......
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
= icon('chevron-down') = icon('chevron-down')
%ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
%li %li
= link_to(s_('ClusterIntegration|Create cluster on Google Container Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project)) = link_to(s_('ClusterIntegration|Create cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
%li %li
= link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project)) = link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
...@@ -4,11 +4,11 @@ ...@@ -4,11 +4,11 @@
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul %ul
%li %li
- link_to_container_engine = link_to(s_('ClusterIntegration|access to Google Container Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer') - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Your account must have %{link_to_container_engine}').html_safe % { link_to_container_engine: link_to_container_engine } = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li %li
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/container-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements } = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li %li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer') - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project } = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.col-sm-4 .col-sm-4
= render 'projects/clusters/sidebar' = render 'projects/clusters/sidebar'
.col-sm-8 .col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine') = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'header' = render 'header'
.row .row
.col-sm-8.col-sm-offset-4.signin-with-google .col-sm-8.col-sm-offset-4.signin-with-google
......
...@@ -5,6 +5,6 @@ ...@@ -5,6 +5,6 @@
.col-sm-4 .col-sm-4
= render 'projects/clusters/sidebar' = render 'projects/clusters/sidebar'
.col-sm-8 .col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine') = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'header' = render 'header'
= render 'form' = render 'form'
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.col-sm-8 .col-sm-8
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration') %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
%p= s_('ClusterIntegration|Create a new cluster on Google Engine right from GitLab') %p= s_('ClusterIntegration|Create a new cluster on Google Kubernetes Engine right from GitLab')
= link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster') %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' = link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
...@@ -2,4 +2,12 @@ ...@@ -2,4 +2,12 @@
= render 'projects/merge_requests/diffs/versions' = render 'projects/merge_requests/diffs/versions'
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
- elsif @merge_request_diff.empty? - elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} .nothing-here-block
= image_tag 'illustrations/merge_request_changes_empty.svg'
%p
Nothing to merge from
%strong= @merge_request.source_branch
into
%strong= @merge_request.target_branch
%p= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") } %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon = issuable_first_contribution_icon
- if access.nonzero? - if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access) %span.note-role.user-access-role= Gitlab::Access.human_access(access)
- if note.resolvable? - if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note) - can_resolve = can?(current_user, :resolve_note, note)
......
- if on_top_of_branch?
- addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
- else
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
%ul.breadcrumb.repo-breadcrumb %ul.breadcrumb.repo-breadcrumb
%li %li
= link_to project_tree_path(@project, @ref) do = link_to project_tree_path(@project, @ref) do
...@@ -8,13 +13,10 @@ ...@@ -8,13 +13,10 @@
- if current_user - if current_user
%li %li
- if !on_top_of_branch? %a.btn.add-to-tree{ addtotree_toggle_attributes }
%span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } = sprite_icon('plus', size: 16, css_class: 'pull-left')
= icon('plus') = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
- else - if on_top_of_branch?
%span.dropdown
%a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" }
= icon('plus')
.add-to-tree-dropdown .add-to-tree-dropdown
%ul.dropdown-menu %ul.dropdown-menu
- if can_edit_tree? - if can_edit_tree?
......
- if outdated_browser? - if outdated_browser?
.browser-alert .flash-container
GitLab may not work properly because you are using an outdated web browser. .flash-alert.text-center
%br GitLab may not work properly because you are using an outdated web browser.
Please install a %br
= link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers') Please install a
for a better experience. = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers')
for a better experience.
- group_member = local_assigns[:group_member] - user = local_assigns.fetch(:user, current_user)
- full_name = true unless local_assigns[:full_name] == false - access = user&.max_member_access_for_group(group.id)
- group_name = full_name ? group.full_name : group.name
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
%li.group-row{ class: css_class }
- if group_member
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do
= sprite_icon('settings')
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
= icon('sign-out')
%li.group-row{ class: ('no-description' if group.description.blank?) }
.stats .stats
%span %span
= icon('bookmark') = icon('bookmark')
...@@ -30,11 +18,10 @@ ...@@ -30,11 +18,10 @@
= link_to group do = link_to group do
= group_icon(group, class: "avatar s40 hidden-xs") = group_icon(group, class: "avatar s40 hidden-xs")
.title .title
= link_to group_name, group, class: 'group-name' = link_to group.full_name, group, class: 'group-name'
- if group_member - if access&.nonzero?
as %span.user-access-role= Gitlab::Access.human_access(access)
%span= group_member.human_access
- if group.description.present? - if group.description.present?
.description .description
......
- if groups.any? - if groups.any?
- user = local_assigns[:user]
%ul.content-list %ul.content-list
- groups.each_with_index do |group, i| - groups.each_with_index do |group, i|
= render "shared/groups/group", group: group = render "shared/groups/group", group: group, user: user
- else - else
.nothing-here-block= s_("GroupsEmptyState|No groups found") .nothing-here-block= s_("GroupsEmptyState|No groups found")
...@@ -5,18 +5,20 @@ ...@@ -5,18 +5,20 @@
- forks = false unless local_assigns[:forks] == true - forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true - ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true - remote = false unless local_assigns[:remote] == true
- load_pipeline_status(projects)
.js-projects-list-holder .js-projects-list-holder
- if any_projects?(projects) - if any_projects?(projects)
- load_pipeline_status(projects)
%ul.projects-list %ul.projects-list
- projects.each_with_index do |project, i| - projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace, = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user
- if @private_forks_count && @private_forks_count > 0 - if @private_forks_count && @private_forks_count > 0
%li.project-row.private-forks-notice %li.project-row.private-forks-notice
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
- forks = false unless local_assigns[:forks] == true - forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true - ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
- access = user&.max_member_access_for_project(project.id) unless user.nil?
- css_class = '' unless local_assigns[:css_class] - css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
...@@ -21,14 +23,19 @@ ...@@ -21,14 +23,19 @@
.project-details .project-details
%h3.prepend-top-0.append-bottom-0 %h3.prepend-top-0.append-bottom-0
= link_to project_path(project), class: 'text-plain' do = link_to project_path(project), class: 'text-plain' do
%span.project-full-name %span.project-full-name><
%span.namespace-name %span.namespace-name
- if project.namespace && !skip_namespace - if project.namespace && !skip_namespace
= project.namespace.human_name = project.namespace.human_name
\/ \/
%span.project-name %span.project-name<
= project.name = project.name
- if access&.nonzero?
-# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements
%span.user-access-role= Gitlab::Access.human_access(access)
- if show_last_commit_as_description - if show_last_commit_as_description
.description.prepend-top-5 .description.prepend-top-5
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
......
---
title: Limit autocomplete menu to applied labels
merge_request: 11110
author: Vitaliy @blackst0ne Klachkov
type: added
---
title: Rename GKE as Kubernetes Engine
merge_request: 15608
author: Takuya Noguchi
type: other
---
title: Using appropiate services in the API for managing forks
merge_request: 15709
author:
type: fixed
---
title: Add untracked files to uploads table
merge_request: 15270
author:
type: other
---
title: Fixed outdated browser flash positioning
merge_request:
author:
type: fixed
---
title: Update empty state page of merge request 'changes' tab
merge_request: 15611
author: Vitaliy @blackst0ne Klachkov
type: added
...@@ -49,6 +49,12 @@ constraints(GroupUrlConstrainer.new) do ...@@ -49,6 +49,12 @@ constraints(GroupUrlConstrainer.new) do
post :resend_invite, on: :member post :resend_invite, on: :member
delete :leave, on: :collection delete :leave, on: :collection
end end
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
end end
scope(path: '*id', scope(path: '*id',
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class SetUploadsPathSizeForMysql < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
# We need at least 297 at the moment. For more detail on that number, see:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/40168#what-is-the-expected-correct-behavior
#
# Rails + PostgreSQL `string` is equivalent to a `text` field, but
# Rails + MySQL `string` is `varchar(255)` by default. Also, note that we
# have an upper limit because with a unique index, MySQL has a max key
# length of 3072 bytes which seems to correspond to `varchar(1024)`.
change_column :uploads, :path, :string, limit: 511
end
def down
# It was unspecified, which is varchar(255) by default in Rails for MySQL.
change_column :uploads, :path, :string
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class TrackUntrackedUploads < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
MIGRATION = 'PrepareUntrackedUploads'
def up
BackgroundMigrationWorker.perform_async(MIGRATION)
end
def down
if table_exists?(:untracked_files_for_uploads)
drop_table :untracked_files_for_uploads
end
end
end
class RescheduleForkNetworkCreationCaller < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'PopulateForkNetworksRange'.freeze
BATCH_SIZE = 100
DELAY_INTERVAL = 15.seconds
disable_ddl_transaction!
class ForkedProjectLink < ActiveRecord::Base
include EachBatch
self.table_name = 'forked_project_links'
end
def up
say 'Populating the `fork_networks` based on existing `forked_project_links`'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
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: 20171124150326) do ActiveRecord::Schema.define(version: 20171205190711) 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"
...@@ -1737,7 +1737,7 @@ ActiveRecord::Schema.define(version: 20171124150326) do ...@@ -1737,7 +1737,7 @@ ActiveRecord::Schema.define(version: 20171124150326) do
create_table "uploads", force: :cascade do |t| create_table "uploads", force: :cascade do |t|
t.integer "size", limit: 8, null: false t.integer "size", limit: 8, null: false
t.string "path", null: false t.string "path", limit: 511, null: false
t.string "checksum", limit: 64 t.string "checksum", limit: 64
t.integer "model_id" t.integer "model_id"
t.string "model_type" t.string "model_type"
......
...@@ -13,13 +13,14 @@ GitLab offers the most scalable Git-based fully integrated platform for software ...@@ -13,13 +13,14 @@ GitLab offers the most scalable Git-based fully integrated platform for software
- **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/), - **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/),
self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com.
- **GitLab Enterprise Edition (EE)** is an [opencore product](https://gitlab.com/gitlab-org/gitlab-ee/), - **GitLab Enterprise Edition (EE)** is an [opencore product](https://gitlab.com/gitlab-org/gitlab-ee/),
self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)** and **GitLab Enterprise Edition Premium (EEP)**. self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**.
- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings). - **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings).
> **GitLab EE** contains all features available in **GitLab CE**, > **GitLab EE** contains all features available in **GitLab CE**,
plus premium features available in each version: **Enterprise Edition Starter** plus premium features available in each version: **Enterprise Edition Starter**
(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in (**EES**), **Enterprise Edition Premium** (**EEP**), and **Enterprise Edition Premium**
**EES** is also available in **EEP**. (**EEU**). Everything available in **EES** is also available in **EEP**. Every feature
available in **EEP** is also available in **EEU**.
---- ----
...@@ -33,7 +34,7 @@ Shortcuts to GitLab's most visited docs: ...@@ -33,7 +34,7 @@ Shortcuts to GitLab's most visited docs:
- [User documentation](user/index.md) - [User documentation](user/index.md)
- [Administrator documentation](#administrator-documentation) - [Administrator documentation](#administrator-documentation)
- [Technical Articles](articles/index.md) - [Contributor documentation](#contributor-documentation)
## Getting started with GitLab ## Getting started with GitLab
......
...@@ -58,6 +58,9 @@ Before proceeding with the Pages configuration, you will need to: ...@@ -58,6 +58,9 @@ Before proceeding with the Pages configuration, you will need to:
so that your users don't have to bring their own. so that your users don't have to bring their own.
1. (Only for custom domains) Have a **secondary IP**. 1. (Only for custom domains) Have a **secondary IP**.
NOTE: **Note:**
If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network.
### DNS configuration ### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider GitLab Pages expect to run on their own virtual host. In your DNS server/provider
......
...@@ -136,7 +136,7 @@ DELETE /projects/:id/protected_branches/:name ...@@ -136,7 +136,7 @@ DELETE /projects/:id/protected_branches/:name
``` ```
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable' curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable'
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
......
...@@ -11,11 +11,11 @@ We made a minimal [Ruby application](https://gitlab.com/gitlab-examples/minimal- ...@@ -11,11 +11,11 @@ We made a minimal [Ruby application](https://gitlab.com/gitlab-examples/minimal-
Let’s start by forking our sample application. Go to [the project page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` button. Soon you should have a project under your namespace with the necessary files. Let’s start by forking our sample application. Go to [the project page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` button. Soon you should have a project under your namespace with the necessary files.
## Setup your own cluster on Google Container Engine ## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. If you do not already have a Google Cloud account, create one at https://console.cloud.google.com.
Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. Visit the [`Kubernetes Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface.
## Connect to Kubernetes cluster ## Connect to Kubernetes cluster
......
# GitLab Helm Chart # GitLab Helm Chart
> **Note**: > **Note**:
* This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview). * This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview).
* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). * These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
...@@ -243,7 +243,7 @@ controller. For `nginx-ingress` you can check the ...@@ -243,7 +243,7 @@ controller. For `nginx-ingress` you can check the
on how to add the annotation to the `controller.service.annotations` array. on how to add the annotation to the `controller.service.annotations` array.
>**Note:** >**Note:**
When using the `nginx-ingress` controller on Google Container Engine (GKE), and using the `external-traffic` annotation, When using the `nginx-ingress` controller on Google Kubernetes Engine (GKE), and using the `external-traffic` annotation,
you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node
as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab. as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab.
See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and
......
# GitLab-Omnibus Helm Chart # GitLab-Omnibus Helm Chart
> **Note:** > **Note:**
* This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). * This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). * These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
...@@ -72,7 +72,7 @@ Other common configuration options: ...@@ -72,7 +72,7 @@ Other common configuration options:
- `baseIP`: the desired [external IP address](#external-ip-recommended) - `baseIP`: the desired [external IP address](#external-ip-recommended)
- `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default. - `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default.
- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart - `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart
- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Container Engine](https://cloud.google.com/container-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/). - `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
For additional configuration options, consult the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-omnibus/values.yaml). For additional configuration options, consult the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-omnibus/values.yaml).
......
# GitLab Runner Helm Chart # GitLab Runner Helm Chart
> **Note:** > **Note:**
These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster. Kubernetes cluster.
......
# Installing GitLab on Kubernetes # Installing GitLab on Kubernetes
> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). > **Note**: These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
to take advantage of GitLab's Helm charts. [Helm] is a package to take advantage of GitLab's Helm charts. [Helm] is a package
......
...@@ -23,12 +23,12 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the ...@@ -23,12 +23,12 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the **Fork** button. Soon you should have a project under your namespace with the
necessary files. necessary files.
## Setup your own cluster on Google Container Engine ## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at If you do not already have a Google Cloud account, create one at
https://console.cloud.google.com. https://console.cloud.google.com.
Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list) Visit the [**Kubernetes Engine**](https://console.cloud.google.com/kubernetes/list)
tab and create a new cluster. You can change the name and leave the rest of the tab and create a new cluster. You can change the name and leave the rest of the
default settings. Once you have your cluster running, you need to connect to the default settings. Once you have your cluster running, you need to connect to the
cluster by following the Google interface. cluster by following the Google interface.
......
...@@ -64,7 +64,7 @@ common actions on issues or merge requests ...@@ -64,7 +64,7 @@ common actions on issues or merge requests
- [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job),
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
- [GKE cluster integration](clusters/index.md): Connecting your GitLab project - [GKE cluster integration](clusters/index.md): Connecting your GitLab project
with Google Container Engine with Google Kubernetes Engine
- [GitLab Pages](pages/index.md): Build, test, and deploy your static - [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages website with GitLab Pages
......
...@@ -367,15 +367,16 @@ module API ...@@ -367,15 +367,16 @@ module API
post ":id/fork/:forked_from_id" do post ":id/fork/:forked_from_id" do
authenticated_as_admin! authenticated_as_admin!
forked_from_project = find_project!(params[:forked_from_id]) fork_from_project = find_project!(params[:forked_from_id])
not_found!("Source Project") unless forked_from_project
if user_project.forked_from_project.nil? not_found!("Source Project") unless fork_from_project
user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
::Projects::ForksCountService.new(forked_from_project).refresh_cache result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)
if result
present user_project.reload, with: Entities::Project
else else
render_api_error!("Project already forked", 409) render_api_error!("Project already forked", 409) if user_project.forked?
end end
end end
...@@ -383,11 +384,11 @@ module API ...@@ -383,11 +384,11 @@ module API
delete ":id/fork" do delete ":id/fork" do
authorize! :remove_fork_project, user_project authorize! :remove_fork_project, user_project
if user_project.forked? result = destroy_conditionally!(user_project) do
destroy_conditionally!(user_project.forked_project_link) ::Projects::UnlinkForkService.new(user_project, current_user).execute
else
not_modified!
end end
result ? status(204) : not_modified!
end end
desc 'Share the project with a group' do desc 'Share the project with a group' do
......
...@@ -39,10 +39,10 @@ module API ...@@ -39,10 +39,10 @@ module API
end end
params do params do
requires :name, type: String, desc: 'The name of the protected branch' requires :name, type: String, desc: 'The name of the protected branch'
optional :push_access_level, type: Integer, default: Gitlab::Access::MASTER, optional :push_access_level, type: Integer,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS, values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to push (defaults: `40`, master access level)' desc: 'Access levels allowed to push (defaults: `40`, master access level)'
optional :merge_access_level, type: Integer, default: Gitlab::Access::MASTER, optional :merge_access_level, type: Integer,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS, values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to merge (defaults: `40`, master access level)' desc: 'Access levels allowed to merge (defaults: `40`, master access level)'
end end
...@@ -52,15 +52,13 @@ module API ...@@ -52,15 +52,13 @@ module API
conflict!("Protected branch '#{params[:name]}' already exists") conflict!("Protected branch '#{params[:name]}' already exists")
end end
protected_branch_params = { # Replace with `declared(params)` after updating to grape v1.0.2
name: params[:name], # See https://github.com/ruby-grape/grape/pull/1710
push_access_levels_attributes: [{ access_level: params[:push_access_level] }], # and https://gitlab.com/gitlab-org/gitlab-ce/issues/40843
merge_access_levels_attributes: [{ access_level: params[:merge_access_level] }] declared_params = params.slice("name", "push_access_level", "merge_access_level", "allowed_to_push", "allowed_to_merge")
}
service_args = [user_project, current_user, protected_branch_params] api_service = ::ProtectedBranches::ApiService.new(user_project, current_user, declared_params)
protected_branch = api_service.create
protected_branch = ::ProtectedBranches::CreateService.new(*service_args).execute
if protected_branch.persisted? if protected_branch.persisted?
present protected_branch, with: Entities::ProtectedBranch, project: user_project present protected_branch, with: Entities::ProtectedBranch, project: user_project
......
...@@ -11,7 +11,7 @@ module Banzai ...@@ -11,7 +11,7 @@ module Banzai
# ref - String reference. # ref - String reference.
# #
# Returns a Project, or nil if the reference can't be found # Returns a Project, or nil if the reference can't be found
def project_from_ref(ref) def parent_from_ref(ref)
return context[:project] unless ref return context[:project] unless ref
Project.find_by_full_path(ref) Project.find_by_full_path(ref)
......
...@@ -82,9 +82,9 @@ module Banzai ...@@ -82,9 +82,9 @@ module Banzai
end end
end end
def project_from_ref_cached(ref) def from_ref_cached(ref)
cached_call(:banzai_project_refs, ref) do cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
project_from_ref(ref) parent_from_ref(ref)
end end
end end
...@@ -153,15 +153,20 @@ module Banzai ...@@ -153,15 +153,20 @@ module Banzai
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_content: nil, link_reference: false) def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
project_path = full_project_path(namespace_ref, project_ref) parent_path = if parent_type == :group
project = project_from_ref_cached(project_path) full_group_path(namespace_ref)
else
full_project_path(namespace_ref, project_ref)
end
if project parent = from_ref_cached(parent_path)
if parent
object = object =
if link_reference if link_reference
find_object_from_link_cached(project, id) find_object_from_link_cached(parent, id)
else else
find_object_cached(project, id) find_object_cached(parent, id)
end end
end end
...@@ -169,13 +174,13 @@ module Banzai ...@@ -169,13 +174,13 @@ module Banzai
title = object_link_title(object) title = object_link_title(object)
klass = reference_class(object_sym) klass = reference_class(object_sym)
data = data_attributes_for(link_content || match, project, object, link: !!link_content) data = data_attributes_for(link_content || match, parent, object, link: !!link_content)
url = url =
if matches.names.include?("url") && matches[:url] if matches.names.include?("url") && matches[:url]
matches[:url] matches[:url]
else else
url_for_object_cached(object, project) url_for_object_cached(object, parent)
end end
content = link_content || object_link_text(object, matches) content = link_content || object_link_text(object, matches)
...@@ -224,17 +229,24 @@ module Banzai ...@@ -224,17 +229,24 @@ module Banzai
# Returns a Hash containing all object references (e.g. issue IDs) per the # Returns a Hash containing all object references (e.g. issue IDs) per the
# project they belong to. # project they belong to.
def references_per_project def references_per_parent
@references_per_project ||= begin @references_per ||= {}
@references_per[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new } refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
nodes.each do |node| nodes.each do |node|
node.to_html.scan(regex) do node.to_html.scan(regex) do
project_path = full_project_path($~[:namespace], $~[:project]) path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
symbol = $~[object_sym] symbol = $~[object_sym]
refs[project_path] << symbol if object_class.reference_valid?(symbol) refs[path] << symbol if object_class.reference_valid?(symbol)
end end
end end
...@@ -244,35 +256,41 @@ module Banzai ...@@ -244,35 +256,41 @@ module Banzai
# Returns a Hash containing referenced projects grouped per their full # Returns a Hash containing referenced projects grouped per their full
# path. # path.
def projects_per_reference def parent_per_reference
@projects_per_reference ||= begin @per_reference ||= {}
@per_reference[parent_type] ||= begin
refs = Set.new refs = Set.new
references_per_project.each do |project_ref, _| references_per_parent.each do |ref, _|
refs << project_ref refs << ref
end end
find_projects_for_paths(refs.to_a).index_by(&:full_path) find_for_paths(refs.to_a).index_by(&:full_path)
end end
end end
def projects_relation_for_paths(paths) def relation_for_paths(paths)
Project.where_full_path_in(paths).includes(:namespace) klass = parent_type.to_s.camelize.constantize
result = klass.where_full_path_in(paths)
return result if parent_type == :group
result.includes(:namespace) if parent_type == :project
end end
# Returns projects for the given paths. # Returns projects for the given paths.
def find_projects_for_paths(paths) def find_for_paths(paths)
if RequestStore.active? if RequestStore.active?
cache = project_refs_cache cache = refs_cache
to_query = paths - cache.keys to_query = paths - cache.keys
unless to_query.empty? unless to_query.empty?
projects = projects_relation_for_paths(to_query) records = relation_for_paths(to_query)
found = [] found = []
projects.each do |project| records.each do |record|
ref = project.full_path ref = record.full_path
get_or_set_cache(cache, ref) { project } get_or_set_cache(cache, ref) { record }
found << ref found << ref
end end
...@@ -284,33 +302,37 @@ module Banzai ...@@ -284,33 +302,37 @@ module Banzai
cache.slice(*paths).values.compact cache.slice(*paths).values.compact
else else
projects_relation_for_paths(paths) relation_for_paths(paths)
end end
end end
def current_project_path def current_parent_path
return unless project @current_parent_path ||= parent&.full_path
@current_project_path ||= project.full_path
end end
def current_project_namespace_path def current_project_namespace_path
return unless project @current_project_namespace_path ||= project&.namespace&.full_path
@current_project_namespace_path ||= project.namespace.full_path
end end
private private
def full_project_path(namespace, project_ref) def full_project_path(namespace, project_ref)
return current_project_path unless project_ref return current_parent_path unless project_ref
namespace_ref = namespace || current_project_namespace_path namespace_ref = namespace || current_project_namespace_path
"#{namespace_ref}/#{project_ref}" "#{namespace_ref}/#{project_ref}"
end end
def project_refs_cache def refs_cache
RequestStore[:banzai_project_refs] ||= {} RequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
end
def parent_type
:project
end
def parent
parent_type == :project ? project : group
end end
end end
end end
......
module Banzai
module Filter
# The actual filter is implemented in the EE mixin
class EpicReferenceFilter < IssuableReferenceFilter
self.reference_type = :epic
def self.object_class
Epic
end
end
end
end
module Banzai
module Filter
class IssuableReferenceFilter < AbstractReferenceFilter
def records_per_parent
@records_per_project ||= {}
@records_per_project[object_class.to_s.underscore] ||= begin
hash = Hash.new { |h, k| h[k] = {} }
parent_per_reference.each do |path, parent|
record_ids = references_per_parent[path]
parent_records(parent, record_ids).each do |record|
hash[parent][record.iid.to_i] = record
end
end
hash
end
end
def find_object(parent, iid)
records_per_parent[parent][iid]
end
def parent_from_ref(ref)
parent_per_reference[ref || current_parent_path]
end
end
end
end
...@@ -8,46 +8,24 @@ module Banzai ...@@ -8,46 +8,24 @@ module Banzai
# When external issues tracker like Jira is activated we should not # When external issues tracker like Jira is activated we should not
# use issue reference pattern, but we should still be able # use issue reference pattern, but we should still be able
# to reference issues from other GitLab projects. # to reference issues from other GitLab projects.
class IssueReferenceFilter < AbstractReferenceFilter class IssueReferenceFilter < IssuableReferenceFilter
self.reference_type = :issue self.reference_type = :issue
def self.object_class def self.object_class
Issue Issue
end end
def find_object(project, iid)
issues_per_project[project][iid]
end
def url_for_object(issue, project) def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true) IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true)
end end
def project_from_ref(ref)
projects_per_reference[ref || current_project_path]
end
# Returns a Hash containing the issues per Project instance.
def issues_per_project
@issues_per_project ||= begin
hash = Hash.new { |h, k| h[k] = {} }
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
issues = project.issues.where(iid: issue_ids.to_a)
issues.each do |issue|
hash[project][issue.iid.to_i] = issue
end
end
hash
end
end
def projects_relation_for_paths(paths) def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service) super(paths).includes(:gitlab_issue_tracker_service)
end end
def parent_records(parent, ids)
parent.issues.where(iid: ids.to_a)
end
end end
end end
end end
...@@ -33,7 +33,7 @@ module Banzai ...@@ -33,7 +33,7 @@ module Banzai
end end
def find_label(project_ref, label_id, label_name) def find_label(project_ref, label_id, label_name)
project = project_from_ref(project_ref) project = parent_from_ref(project_ref)
return unless project return unless project
label_params = label_params(label_id, label_name) label_params = label_params(label_id, label_name)
...@@ -66,7 +66,7 @@ module Banzai ...@@ -66,7 +66,7 @@ module Banzai
def object_link_text(object, matches) def object_link_text(object, matches)
project_path = full_project_path(matches[:namespace], matches[:project]) project_path = full_project_path(matches[:namespace], matches[:project])
project_from_ref = project_from_ref_cached(project_path) project_from_ref = from_ref_cached(project_path)
reference = project_from_ref.to_human_reference(project) reference = project_from_ref.to_human_reference(project)
label_suffix = " <i>in #{reference}</i>" if reference.present? label_suffix = " <i>in #{reference}</i>" if reference.present?
......
...@@ -4,48 +4,19 @@ module Banzai ...@@ -4,48 +4,19 @@ module Banzai
# to merge requests that do not exist are ignored. # to merge requests that do not exist are ignored.
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class MergeRequestReferenceFilter < AbstractReferenceFilter class MergeRequestReferenceFilter < IssuableReferenceFilter
self.reference_type = :merge_request self.reference_type = :merge_request
def self.object_class def self.object_class
MergeRequest MergeRequest
end end
def find_object(project, iid)
merge_requests_per_project[project][iid]
end
def url_for_object(mr, project) def url_for_object(mr, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
h.project_merge_request_url(project, mr, h.project_merge_request_url(project, mr,
only_path: context[:only_path]) only_path: context[:only_path])
end end
def project_from_ref(ref)
projects_per_reference[ref || current_project_path]
end
# Returns a Hash containing the merge_requests per Project instance.
def merge_requests_per_project
@merge_requests_per_project ||= begin
hash = Hash.new { |h, k| h[k] = {} }
projects_per_reference.each do |path, project|
merge_request_ids = references_per_project[path]
merge_requests = project.merge_requests
.where(iid: merge_request_ids.to_a)
.includes(target_project: :namespace)
merge_requests.each do |merge_request|
hash[project][merge_request.iid.to_i] = merge_request
end
end
hash
end
end
def object_link_text_extras(object, matches) def object_link_text_extras(object, matches)
extras = super extras = super
...@@ -61,6 +32,12 @@ module Banzai ...@@ -61,6 +32,12 @@ module Banzai
extras extras
end end
def parent_records(parent, ids)
parent.merge_requests
.where(iid: ids.to_a)
.includes(target_project: :namespace)
end
end end
end end
end end
...@@ -38,7 +38,7 @@ module Banzai ...@@ -38,7 +38,7 @@ module Banzai
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
project_path = full_project_path(namespace_ref, project_ref) project_path = full_project_path(namespace_ref, project_ref)
project = project_from_ref(project_path) project = parent_from_ref(project_path)
return unless project return unless project
......
...@@ -8,7 +8,7 @@ module Banzai ...@@ -8,7 +8,7 @@ module Banzai
# #
class UploadLinkFilter < HTML::Pipeline::Filter class UploadLinkFilter < HTML::Pipeline::Filter
def call def call
return doc unless project return doc unless project || group
doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el| doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el|
process_link_attr el.attribute('href') process_link_attr el.attribute('href')
...@@ -28,13 +28,27 @@ module Banzai ...@@ -28,13 +28,27 @@ module Banzai
end end
def build_url(uri) def build_url(uri)
File.join(Gitlab.config.gitlab.url, project.full_path, uri) base_path = Gitlab.config.gitlab.url
if group
urls = Gitlab::Routing.url_helpers
# we need to get last 2 parts of the uri which are secret and filename
uri_parts = uri.split(File::SEPARATOR)
file_path = urls.show_group_uploads_path(group, uri_parts[-2], uri_parts[-1])
File.join(base_path, file_path)
else
File.join(base_path, project.full_path, uri)
end
end end
def project def project
context[:project] context[:project]
end end
def group
context[:group]
end
# Ensure that a :project key exists in context # Ensure that a :project key exists in context
# #
# Note that while the key might exist, its value could be nil! # Note that while the key might exist, its value could be nil!
......
...@@ -28,8 +28,8 @@ module Banzai ...@@ -28,8 +28,8 @@ module Banzai
issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user) issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user)
merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user) merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user)
issuables_for_nodes = issue_parser.issues_for_nodes(nodes).merge( issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge(
merge_request_parser.merge_requests_for_nodes(nodes) merge_request_parser.records_for_nodes(nodes)
) )
# The project for the issue/MR might be pending for deletion! # The project for the issue/MR might be pending for deletion!
......
module Banzai
module ReferenceParser
# The actual parser is implemented in the EE mixin
class EpicParser < IssuableParser
self.reference_type = :epic
def records_for_nodes(_nodes)
{}
end
end
end
end
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