diff options
266 files changed, 5209 insertions, 2028 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 6adbda53456..556a5d11a39 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -510,6 +510,15 @@ Metrics/PerceivedComplexity: #################### Lint ################################ +# Checks for useless access modifiers. +Lint/UselessAccessModifier: + Enabled: true + +# Checks for attempts to use `private` or `protected` to set the visibility +# of a class method, which does not work. +Lint/IneffectiveAccessModifier: + Enabled: false + # Checks for ambiguous operators in the first argument of a method invocation # without parentheses. Lint/AmbiguousOperator: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b622b9239d4..76ae5952753 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -19,10 +19,6 @@ Lint/AssignmentInCondition: Lint/HandleExceptions: Enabled: false -# Offense count: 21 -Lint/IneffectiveAccessModifier: - Enabled: false - # Offense count: 2 Lint/Loop: Enabled: false @@ -48,10 +44,6 @@ Lint/UnusedBlockArgument: Lint/UnusedMethodArgument: Enabled: false -# Offense count: 11 -Lint/UselessAccessModifier: - Enabled: false - # Offense count: 12 # Cop supports --auto-correct. Performance/PushSplat: diff --git a/CHANGELOG b/CHANGELOG index a4dbce95268..963fec597d5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,29 +1,64 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.11.0 (unreleased) + - Fix the title of the toggle dropdown button. !5515 (herminiotorres) + - Improve diff performance by eliminating redundant checks for text blobs - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) + - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Fix CI status icon link underline (ClemMakesApps) + - The Repository class is now instrumented + - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes + - Add "No one can push" as an option for protected branches. !5081 + - Environments have an url to link to - Limit git rev-list output count to one in forced push check - Clean up unused routes (Josef Strzibny) - Add green outline to New Branch button. !5447 (winniehell) + - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings + - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented + - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 + - Include old revision in merge request update hooks (Ben Boeckel) - Add build event color in HipChat messages (David Eisner) - Make fork counter always clickable. !5463 (winniehell) + - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333 + - The overhead of instrumented method calls has been reduced - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` + - Bump gitlab_git to speedup DiffCollection iterations - Make branches sortable without push permission !5462 (winniehell) + - Check for Ci::Build artifacts at database level on pipeline partial + - Make "New issue" button in Issue page less obtrusive !5457 (winniehell) + - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration + - Fix search for notes which belongs to deleted objects - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed + - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab. + - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible + - Add commit stats in commit api. !5517 (dixpac) + - Add CI configuration button on project page - Make error pages responsive (Takuya Noguchi) - -v 8.10.3 (unreleased) + - Change requests_profiles resource constraint to catch virtually any file + - Reduce number of queries made for merge_requests/:id/diffs + - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) + - Fix RequestProfiler::Middleware error when code is reloaded in development + - Catch what warden might throw when profiling requests to re-throw it + +v 8.10.3 + - Fix Import/Export issue importing milestones and labels not associated properly. !5426 + - Fix timing problems running imports on production. !5523 + - Add a log message when a project is scheduled for destruction for debugging. !5540 + - Fix hooks missing on imported GitLab projects. !5549 + - Properly abort a merge when merge conflicts occur. !5569 + - Fix importer for GitHub Pull Requests when a branch was removed. !5573 + - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584 + - Trim extra displayed carriage returns in diffs and files with CRLFs. !5588 v 8.10.2 - User can now search branches by name. !5144 @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.3.2' +gem 'gitlab_git', '~> 10.4.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 670578dec6d..7b4175ea824 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.3.2) + gitlab_git (10.4.2) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -870,7 +870,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.3.2) + gitlab_git (~> 10.4.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d212d66da1b..9e6901962c6 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -171,6 +171,11 @@ break; case 'search:show': new Search(); + break; + case 'projects:protected_branches:index': + new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true); + new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/javascripts/lib/utils/md5.js b/app/assets/javascripts/lib/utils/md5.js deleted file mode 100644 index b63716eaad2..00000000000 --- a/app/assets/javascripts/lib/utils/md5.js +++ /dev/null @@ -1,211 +0,0 @@ -function md5 (str) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + namespaced by: Michael White (http://getsprink.com) - // + tweaked by: Jack - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // - depends on: utf8_encode - // * example 1: md5('Kevin van Zonneveld'); - // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9' - var xl; - - var rotateLeft = function (lValue, iShiftBits) { - return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); - }; - - var addUnsigned = function (lX, lY) { - var lX4, lY4, lX8, lY8, lResult; - lX8 = (lX & 0x80000000); - lY8 = (lY & 0x80000000); - lX4 = (lX & 0x40000000); - lY4 = (lY & 0x40000000); - lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); - if (lX4 & lY4) { - return (lResult ^ 0x80000000 ^ lX8 ^ lY8); - } - if (lX4 | lY4) { - if (lResult & 0x40000000) { - return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); - } else { - return (lResult ^ 0x40000000 ^ lX8 ^ lY8); - } - } else { - return (lResult ^ lX8 ^ lY8); - } - }; - - var _F = function (x, y, z) { - return (x & y) | ((~x) & z); - }; - var _G = function (x, y, z) { - return (x & z) | (y & (~z)); - }; - var _H = function (x, y, z) { - return (x ^ y ^ z); - }; - var _I = function (x, y, z) { - return (y ^ (x | (~z))); - }; - - var _FF = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _GG = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _HH = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _II = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var convertToWordArray = function (str) { - var lWordCount; - var lMessageLength = str.length; - var lNumberOfWords_temp1 = lMessageLength + 8; - var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; - var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; - var lWordArray = new Array(lNumberOfWords - 1); - var lBytePosition = 0; - var lByteCount = 0; - while (lByteCount < lMessageLength) { - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); - lByteCount++; - } - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); - lWordArray[lNumberOfWords - 2] = lMessageLength << 3; - lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; - return lWordArray; - }; - - var wordToHex = function (lValue) { - var wordToHexValue = "", - wordToHexValue_temp = "", - lByte, lCount; - for (lCount = 0; lCount <= 3; lCount++) { - lByte = (lValue >>> (lCount * 8)) & 255; - wordToHexValue_temp = "0" + lByte.toString(16); - wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); - } - return wordToHexValue; - }; - - var x = [], - k, AA, BB, CC, DD, a, b, c, d, S11 = 7, - S12 = 12, - S13 = 17, - S14 = 22, - S21 = 5, - S22 = 9, - S23 = 14, - S24 = 20, - S31 = 4, - S32 = 11, - S33 = 16, - S34 = 23, - S41 = 6, - S42 = 10, - S43 = 15, - S44 = 21; - - str = this.utf8_encode(str); - x = convertToWordArray(str); - a = 0x67452301; - b = 0xEFCDAB89; - c = 0x98BADCFE; - d = 0x10325476; - - xl = x.length; - for (k = 0; k < xl; k += 16) { - AA = a; - BB = b; - CC = c; - DD = d; - a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); - d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); - c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB); - b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); - a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); - d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); - c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613); - b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501); - a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8); - d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); - c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); - b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); - a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122); - d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193); - c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E); - b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821); - a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); - d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340); - c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); - b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); - a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); - d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453); - c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); - b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); - a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); - d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); - c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); - b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); - a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); - d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); - c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); - b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); - a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); - d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681); - c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); - b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); - a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); - d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); - c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); - b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); - a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); - d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); - c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); - b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05); - a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); - d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); - c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); - b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); - a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244); - d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97); - c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); - b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039); - a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3); - d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); - c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); - b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1); - a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); - d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); - c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314); - b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); - a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82); - d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); - c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); - b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391); - a = addUnsigned(a, AA); - b = addUnsigned(b, BB); - c = addUnsigned(c, CC); - d = addUnsigned(d, DD); - } - - var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); - - return temp.toLowerCase(); -} diff --git a/app/assets/javascripts/lib/utils/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js deleted file mode 100644 index 39ffe44dae0..00000000000 --- a/app/assets/javascripts/lib/utils/utf8_encode.js +++ /dev/null @@ -1,70 +0,0 @@ -function utf8_encode (argString) {
- // http://kevin.vanzonneveld.net
- // + original by: Webtoolkit.info (http://www.webtoolkit.info/)
- // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
- // + improved by: sowberry
- // + tweaked by: Jack
- // + bugfixed by: Onno Marsman
- // + improved by: Yves Sucaet
- // + bugfixed by: Onno Marsman
- // + bugfixed by: Ulrich
- // + bugfixed by: Rafal Kukawski
- // + improved by: kirilloid
- // + bugfixed by: kirilloid
- // * example 1: utf8_encode('Kevin van Zonneveld');
- // * returns 1: 'Kevin van Zonneveld'
-
- if (argString === null || typeof argString === "undefined") {
- return "";
- }
-
- var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
- var utftext = '',
- start, end, stringl = 0;
-
- start = end = 0;
- stringl = string.length;
- for (var n = 0; n < stringl; n++) {
- var c1 = string.charCodeAt(n);
- var enc = null;
-
- if (c1 < 128) {
- end++;
- } else if (c1 > 127 && c1 < 2048) {
- enc = String.fromCharCode(
- (c1 >> 6) | 192,
- ( c1 & 63) | 128
- );
- } else if (c1 & 0xF800 != 0xD800) {
- enc = String.fromCharCode(
- (c1 >> 12) | 224,
- ((c1 >> 6) & 63) | 128,
- ( c1 & 63) | 128
- );
- } else { // surrogate pairs
- if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); }
- var c2 = string.charCodeAt(++n);
- if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); }
- c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
- enc = String.fromCharCode(
- (c1 >> 18) | 240,
- ((c1 >> 12) & 63) | 128,
- ((c1 >> 6) & 63) | 128,
- ( c1 & 63) | 128
- );
- }
- if (enc !== null) {
- if (end > start) {
- utftext += string.slice(start, end);
- }
- utftext += enc;
- start = end = n + 1;
- }
- }
-
- if (end > start) {
- utftext += string.slice(start, stringl);
- }
-
- return utftext;
-}
diff --git a/app/assets/javascripts/protected_branches.js b/app/assets/javascripts/protected_branches.js deleted file mode 100644 index db21a19964d..00000000000 --- a/app/assets/javascripts/protected_branches.js +++ /dev/null @@ -1,35 +0,0 @@ -(function() { - $(function() { - return $(".protected-branches-list :checkbox").change(function(e) { - var can_push, id, name, obj, url; - name = $(this).attr("name"); - if (name === "developers_can_push" || name === "developers_can_merge") { - id = $(this).val(); - can_push = $(this).is(":checked"); - url = $(this).data("url"); - return $.ajax({ - type: "PATCH", - url: url, - dataType: "json", - data: { - id: id, - protected_branch: ( - obj = {}, - obj["" + name] = can_push, - obj - ) - }, - success: function() { - var row; - row = $(e.target); - return row.closest('tr').effect('highlight'); - }, - error: function() { - return new Flash("Failed to update branch!", "alert"); - } - }); - } - }); - }); - -}).call(this); diff --git a/app/assets/javascripts/protected_branches_access_select.js.es6 b/app/assets/javascripts/protected_branches_access_select.js.es6 new file mode 100644 index 00000000000..e98312bbf37 --- /dev/null +++ b/app/assets/javascripts/protected_branches_access_select.js.es6 @@ -0,0 +1,63 @@ +class ProtectedBranchesAccessSelect { + constructor(container, saveOnSelect, selectDefault) { + this.container = container; + this.saveOnSelect = saveOnSelect; + + this.container.find(".allowed-to-merge").each((i, element) => { + var fieldName = $(element).data('field-name'); + var dropdown = $(element).glDropdown({ + data: gon.merge_access_levels, + selectable: true, + fieldName: fieldName, + clicked: _.chain(this.onSelect).partial(element).bind(this).value() + }); + + if (selectDefault) { + dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); + } + }); + + + this.container.find(".allowed-to-push").each((i, element) => { + var fieldName = $(element).data('field-name'); + var dropdown = $(element).glDropdown({ + data: gon.push_access_levels, + selectable: true, + fieldName: fieldName, + clicked: _.chain(this.onSelect).partial(element).bind(this).value() + }); + + if (selectDefault) { + dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); + } + }); + } + + onSelect(dropdown, selected, element, e) { + $(dropdown).find('.dropdown-toggle-text').text(selected.text); + if (this.saveOnSelect) { + return $.ajax({ + type: "POST", + url: $(dropdown).data('url'), + dataType: "json", + data: { + _method: 'PATCH', + id: $(dropdown).data('id'), + protected_branch: { + ["" + ($(dropdown).data('type')) + "_attributes"]: { + "access_level": selected.id + } + } + }, + success: function() { + var row; + row = $(e.target); + return row.closest('tr').effect('highlight'); + }, + error: function() { + return new Flash("Failed to update branch!", "alert"); + } + }); + } + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0298577c494..cbc980f52ff 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -61,6 +61,10 @@ font-size: 0; } + .ci-status-link { + display: inline-block; + } + .btn-clipboard, .btn-transparent { padding-left: 0; padding-right: 0; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index ee3b2d2b801..dfe1e3075da 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -99,3 +99,33 @@ form.edit-issue { .issue-form .select2-container { width: 250px !important; } + +.issues-footer { + padding-top: $gl-padding; + padding-bottom: 37px; +} + +.issue-email-modal-btn { + padding: 0; + color: $gl-link-color; + background-color: transparent; + border: 0; + outline: 0; + + &:hover { + text-decoration: underline; + } +} + +.email-modal-input-group { + margin-bottom: 10px; + + .form-control { + background-color: $white-light; + } + + .btn { + background-color: $background-color; + border: 1px solid $border-gray-light; + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index c58e2ffe7f5..21919fe4d73 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -18,6 +18,10 @@ .btn { margin: 4px; } + + .table.builds { + min-width: 1200px; + } } .content-list { @@ -35,7 +39,7 @@ } .table.builds { - min-width: 1200px; + min-width: 900px; &.pipeline { min-width: 650px; @@ -128,7 +132,7 @@ .icon-container { display: inline-block; text-align: right; - width: 20px; + width: 15px; .fa { position: relative; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index cc3aef5199e..4409477916f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -661,14 +661,28 @@ pre.light-well { } } +.new_protected_branch { + .dropdown { + display: inline; + margin-left: 15px; + } + + label { + min-width: 120px; + } +} + .protected-branches-list { a { color: $gl-gray; - font-weight: 600; &:hover { color: $gl-link-color; } + + &.is-active { + font-weight: 600; + } } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a1004d9bcea..634d36a4467 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -243,42 +243,6 @@ class ApplicationController < ActionController::Base end end - def set_filters_params - set_default_sort - - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - - @sort = params[:sort] - @filter_params = params.dup - - if @project - @filter_params[:project_id] = @project.id - elsif @group - @filter_params[:group_id] = @group.id - else - # TODO: this filter ignore issues/mr created in public or - # internal repos where you are not a member. Enable this filter - # or improve current implementation to filter only issues you - # created or assigned or mentioned - # @filter_params[:authorized_only] = true - end - - @filter_params - end - - def get_issues_collection - set_filters_params - @issuable_finder = IssuesFinder.new(current_user, @filter_params) - @issuable_finder.execute - end - - def get_merge_requests_collection - set_filters_params - @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params) - @issuable_finder.execute - end - def import_sources_enabled? !current_application_settings.import_sources.empty? end @@ -363,24 +327,4 @@ class ApplicationController < ActionController::Base def u2f_app_id request.base_url end - - private - - def set_default_sort - key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests') - 'issuable_sort' - end - - cookies[key] = params[:sort] if key && params[:sort].present? - params[:sort] = cookies[key] if key - params[:sort] ||= 'id_desc' - end - - def is_a_listing_page_for?(page_type) - controller_name, action_name = params.values_at(:controller, :action) - - (controller_name == "projects/#{page_type}" && action_name == 'index') || - (controller_name == 'groups' && action_name == page_type) || - (controller_name == 'dashboard' && action_name == page_type) - end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb new file mode 100644 index 00000000000..c802922e0af --- /dev/null +++ b/app/controllers/concerns/issuable_collections.rb @@ -0,0 +1,79 @@ +module IssuableCollections + extend ActiveSupport::Concern + include SortingHelper + + included do + helper_method :issues_finder + helper_method :merge_requests_finder + end + + private + + def issues_collection + issues_finder.execute + end + + def merge_requests_collection + merge_requests_finder.execute + end + + def issues_finder + @issues_finder ||= issuable_finder_for(IssuesFinder) + end + + def merge_requests_finder + @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) + end + + def issuable_finder_for(finder_class) + finder_class.new(current_user, filter_params) + end + + def filter_params + set_sort_order_from_cookie + set_default_scope + set_default_state + + @filter_params = params.dup + @filter_params[:sort] ||= default_sort_order + + @sort = @filter_params[:sort] + + if @project + @filter_params[:project_id] = @project.id + elsif @group + @filter_params[:group_id] = @group.id + else + # TODO: this filter ignore issues/mr created in public or + # internal repos where you are not a member. Enable this filter + # or improve current implementation to filter only issues you + # created or assigned or mentioned + # @filter_params[:authorized_only] = true + end + + @filter_params + end + + def set_default_scope + params[:scope] = 'all' if params[:scope].blank? + end + + def set_default_state + params[:state] = 'opened' if params[:state].blank? + end + + def set_sort_order_from_cookie + key = 'issuable_sort' + + cookies[key] = params[:sort] if params[:sort].present? + params[:sort] = cookies[key] + end + + def default_sort_order + case params[:state] + when 'opened', 'all' then sort_value_recently_created + when 'merged', 'closed' then sort_value_recently_updated + else sort_value_recently_created + end + end +end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 4feabc32b1c..b89fb94be6e 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -1,12 +1,14 @@ module IssuesAction extend ActiveSupport::Concern + include IssuableCollections def issues - @issues = get_issues_collection.non_archived - @issues = @issues.page(params[:page]) - @issues = @issues.preload(:author, :project) + @label = issues_finder.labels.first - @label = @issuable_finder.labels.first + @issues = issues_collection + .non_archived + .preload(:author, :project) + .page(params[:page]) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 06a6b065e7e..a1b0eee37f9 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -1,11 +1,13 @@ module MergeRequestsAction extend ActiveSupport::Concern + include IssuableCollections def merge_requests - @merge_requests = get_merge_requests_collection.non_archived - @merge_requests = @merge_requests.page(params[:page]) - @merge_requests = @merge_requests.preload(:author, :target_project) + @label = merge_requests_finder.labels.first - @label = @issuable_finder.labels.first + @merge_requests = merge_requests_collection + .non_archived + .preload(:author, :target_project) + .page(params[:page]) end end diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index 461fc059a3c..a1ab8b99048 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,5 +1,5 @@ class Explore::ApplicationController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked + skip_before_action :authenticate_user!, :reject_blocked! layout 'explore' end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index f7b44099b78..4eca278599f 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,5 +1,5 @@ class HelpController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked + skip_before_action :authenticate_user!, :reject_blocked! layout 'help' diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 25e58724860..944c73d139a 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -82,8 +82,6 @@ class Import::BitbucketController < Import::BaseController go_to_bitbucket_for_permissions end - private - def access_params { bitbucket_access_token: session[:bitbucket_access_token], diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 23a396e8084..08130ee8176 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -61,8 +61,6 @@ class Import::GitlabController < Import::BaseController go_to_gitlab_for_permissions end - private - def access_params { gitlab_access_token: session[:gitlab_access_token] } end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 6126acccaab..e926043f3eb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -6,6 +6,7 @@ class Projects::BranchesController < Projects::ApplicationController before_action :authorize_push_code!, only: [:new, :create, :destroy] def index + @sort = params[:sort].presence || 'name' @branches = BranchesFinder.new(@repository, params).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 4b433796161..58678f96879 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,8 +2,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:destroy] - before_action :environment, only: [:show, :destroy] + before_action :authorize_update_environment!, only: [:edit, :update, :destroy] + before_action :environment, only: [:show, :edit, :update, :destroy] def index @environments = project.environments @@ -17,13 +17,24 @@ class Projects::EnvironmentsController < Projects::ApplicationController @environment = project.environments.new end + def edit + end + def create - @environment = project.environments.create(create_params) + @environment = project.environments.create(environment_params) if @environment.persisted? redirect_to namespace_project_environment_path(project.namespace, project, @environment) else - render 'new' + render :new + end + end + + def update + if @environment.update(environment_params) + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + else + render :edit end end @@ -39,8 +50,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController private - def create_params - params.require(:environment).permit(:name) + def environment_params + params.require(:environment).permit(:name, :external_url) end def environment diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 91ff9407216..7f5c3ff3d6a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,6 +3,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleSubscriptionAction include IssuableActions include ToggleAwardEmoji + include IssuableCollections before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, @@ -24,7 +25,7 @@ class Projects::IssuesController < Projects::ApplicationController def index terms = params['issue_search'] - @issues = get_issues_collection + @issues = issues_collection if terms.present? if terms =~ /\A#(\d+)\z/ @@ -82,7 +83,7 @@ class Projects::IssuesController < Projects::ApplicationController end def create - @issue = Issues::CreateService.new(project, current_user, issue_params).execute + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute respond_to do |format| format.html do @@ -92,7 +93,7 @@ class Projects::IssuesController < Projects::ApplicationController render :new end end - format.js do |format| + format.js do @link = @issue.attachment.url.to_js end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 23252fa59cc..116e7904a4e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -5,6 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include IssuableActions include NotesHelper include ToggleAwardEmoji + include IssuableCollections before_action :module_enabled before_action :merge_request, only: [ @@ -29,7 +30,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index terms = params['issue_search'] - @merge_requests = get_merge_requests_collection + @merge_requests = merge_requests_collection if terms.present? if terms =~ /\A[#!](\d+)\z/ @@ -377,6 +378,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController fresh. discussions + preload_noteable_for_regular_notes(@discussions.flat_map(&:notes)) + # This is not executed lazily @notes = Banzai::NoteRenderer.render( @discussions.flat_map(&:notes), @@ -407,7 +410,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController } @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? - @grouped_diff_discussions = @merge_request.notes.grouped_diff_discussions + @grouped_diff_discussions = @merge_request.notes.inc_author_project_award_emoji.grouped_diff_discussions Banzai::NoteRenderer.render( @grouped_diff_discussions.values.flat_map(&:notes), diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 10dca47fded..d28ec6e2eac 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -3,19 +3,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController before_action :require_non_empty_project before_action :authorize_admin_project! before_action :load_protected_branch, only: [:show, :update, :destroy] + before_action :load_protected_branches, only: [:index] layout "project_settings" def index - @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new - gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }) + load_protected_branches_gon_variables end def create - @project.protected_branches.create(protected_branch_params) - redirect_to namespace_project_protected_branches_path(@project.namespace, - @project) + @protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute + if @protected_branch.persisted? + redirect_to namespace_project_protected_branches_path(@project.namespace, @project) + else + load_protected_branches + load_protected_branches_gon_variables + render :index + end end def show @@ -23,7 +28,9 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def update - if @protected_branch && @protected_branch.update_attributes(protected_branch_params) + @protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) + + if @protected_branch.valid? respond_to do |format| format.json { render json: @protected_branch, status: :ok } end @@ -50,6 +57,18 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def protected_branch_params - params.require(:protected_branch).permit(:name, :developers_can_push, :developers_can_merge) + params.require(:protected_branch).permit(:name, + merge_access_level_attributes: [:access_level], + push_access_level_attributes: [:access_level]) + end + + def load_protected_branches + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) + end + + def load_protected_branches_gon_variables + gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }, + push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } }, + merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } }) end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ec7a2e63b9a..a6e1aa5ccc1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController end if @project.pending_delete? - flash[:alert] = "Project queued for delete." + flash[:alert] = "Project #{@project.name} queued for deletion." end respond_to do |format| diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 69c92d2bed2..61517d21f9f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ class SearchController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked + skip_before_action :authenticate_user!, :reject_blocked! include SearchHelper diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index a0932712bd0..33daac0399e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -109,7 +109,7 @@ class IssuableFinder scope.where(title: params[:milestone_title]) else - nil + Milestone.none end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 03495cf5ec4..50de93d4bdf 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -245,7 +245,6 @@ module ApplicationHelper milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], - sort: params[:sort], issue_search: params[:issue_search], label_name: params[:label_name] } diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index abe115d8c68..48c27828219 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -13,7 +13,7 @@ module BlobHelper blob = project.repository.blob_at(ref, path) rescue nil - return unless blob && blob_text_viewable?(blob) + return unless blob from_mr = options[:from_merge_request_id] link_opts = {} diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 4c031942793..f35e2f6ddcd 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -144,8 +144,6 @@ module DiffHelper toggle_whitespace_link(url, options) end - private - def hide_whitespace? params[:w] == '1' end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 0c47abe0fba..26bde2230a9 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -92,6 +92,10 @@ module NotesHelper project.team.max_member_access_for_user_ids(user_ids) end + def preload_noteable_for_regular_notes(notes) + ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable) + end + def note_max_access_for_user(note) note.project.team.human_max_access(note.author_id) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a733dff1579..505545fbabb 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -263,6 +263,10 @@ module ProjectsHelper filename_path(project, :version) end + def ci_configuration_path(project) + filename_path(project, :gitlab_ci_yml) + end + def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } namespace_project_wiki_path(proj.namespace, proj, page, url_params) diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index d86f1999f5c..e1c0b497550 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -102,11 +102,11 @@ module SortingHelper end def sort_value_oldest_created - 'id_asc' + 'created_asc' end def sort_value_recently_created - 'id_desc' + 'created_desc' end def sort_value_milestone_soon diff --git a/app/models/ability.rb b/app/models/ability.rb index e47c5539f60..d95a2507199 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -47,6 +47,16 @@ class Ability end end + # Returns an Array of Issues that can be read by the given user. + # + # issues - The issues to reduce down to those readable by the user. + # user - The User for which to check the issues + def issues_readable_by_user(issues, user = nil) + return issues if user && user.admin? + + issues.select { |issue| issue.visible_to_user?(user) } + end + # List of possible abilities for anonymous user def anonymous_abilities(user, subject) if subject.is_a?(PersonalSnippet) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cbfa14e81f1..08f396210c9 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -13,6 +13,7 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) } + scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual) } @@ -331,7 +332,7 @@ module Ci end def valid_token?(token) - project.valid_runners_token? token + project.valid_runners_token?(token) end def has_tags? diff --git a/app/models/commit.rb b/app/models/commit.rb index f80f1063406..486ad6714d9 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -178,7 +178,18 @@ class Commit end def author - @author ||= User.find_by_any_email(author_email.downcase) + if RequestStore.active? + key = "commit_author:#{author_email.downcase}" + # nil is a valid value since no author may exist in the system + if RequestStore.store.has_key?(key) + @author = RequestStore.store[key] + else + @author = find_author_by_any_email + RequestStore.store[key] = @author + end + else + @author ||= find_author_by_any_email + end end def committer @@ -306,6 +317,10 @@ class Commit private + def find_author_by_any_email + User.find_by_any_email(author_email.downcase) + end + def repo_changes changes = { added: [], modified: [], removed: [] } diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index acb6f5a2998..cbae1cd439b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -17,7 +17,7 @@ module Issuable belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :milestone - has_many :notes, as: :noteable, dependent: :destroy do + has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do def authors_loaded? # We check first if we're loaded to not load unnecessarily. loaded? && to_a.all? { |note| note.association(:author).loaded? } diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb new file mode 100644 index 00000000000..3b8e6df2da9 --- /dev/null +++ b/app/models/concerns/spammable.rb @@ -0,0 +1,16 @@ +module Spammable + extend ActiveSupport::Concern + + included do + attr_accessor :spam + after_validation :check_for_spam, on: :create + end + + def spam? + @spam + end + + def check_for_spam + self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? + end +end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 885deaf78d2..24c7b26d223 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -1,12 +1,26 @@ module TokenAuthenticatable extend ActiveSupport::Concern + private + + def write_new_token(token_field) + new_token = generate_token(token_field) + write_attribute(token_field, new_token) + end + + def generate_token(token_field) + loop do + token = Devise.friendly_token + break token unless self.class.unscoped.find_by(token_field => token) + end + end + class_methods do def authentication_token_fields @token_fields || [] end - private + private # rubocop:disable Lint/UselessAccessModifier def add_authentication_token_field(token_field) @token_fields = [] unless @token_fields @@ -32,18 +46,4 @@ module TokenAuthenticatable end end end - - private - - def write_new_token(token_field) - new_token = generate_token(token_field) - write_attribute(token_field, new_token) - end - - def generate_token(token_field) - loop do - token = Devise.friendly_token - break token unless self.class.unscoped.find_by(token_field => token) - end - end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 9671955db36..c816deb4e0c 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -67,7 +67,7 @@ class DiffNote < Note return false unless supported? return true if for_commit? - diff_refs ||= self.noteable.diff_refs + diff_refs ||= noteable_diff_refs self.position.diff_refs == diff_refs end @@ -78,6 +78,14 @@ class DiffNote < Note !self.for_merge_request? || self.noteable.support_new_diff_notes? end + def noteable_diff_refs + if noteable.respond_to?(:diff_sha_refs) + noteable.diff_sha_refs + else + noteable.diff_refs + end + end + def set_original_position self.original_position = self.position.dup end @@ -96,7 +104,7 @@ class DiffNote < Note self.project, nil, old_diff_refs: self.position.diff_refs, - new_diff_refs: self.noteable.diff_refs, + new_diff_refs: noteable_diff_refs, paths: self.position.paths ).execute(self) end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 74facfd1c9c..e2218a5f02b 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -49,6 +49,12 @@ class Discussion self.noteable == target && !diff_discussion? end + def active? + return @active if defined?(@active) + + @active = first_note.active? + end + def expanded? !diff_discussion? || active? end diff --git a/app/models/environment.rb b/app/models/environment.rb index ac3a571a1f3..baed106e8c8 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -3,6 +3,8 @@ class Environment < ActiveRecord::Base has_many :deployments + before_validation :nullify_external_url + validates :name, presence: true, uniqueness: { scope: :project_id }, @@ -10,7 +12,17 @@ class Environment < ActiveRecord::Base format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :external_url, + uniqueness: { scope: :project_id }, + length: { maximum: 255 }, + allow_nil: true, + addressable_url: true + def last_deployment deployments.last end + + def nullify_external_url + self.external_url = nil if self.external_url.blank? + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 60af8c15340..11f734cfc6d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -6,6 +6,7 @@ class Issue < ActiveRecord::Base include Referable include Sortable include Taskable + include Spammable DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -229,6 +230,34 @@ class Issue < ActiveRecord::Base self.closed_by_merge_requests(current_user).empty? end + # Returns `true` if the current issue can be viewed by either a logged in User + # or an anonymous user. + def visible_to_user?(user = nil) + user ? readable_by?(user) : publicly_visible? + end + + # Returns `true` if the given User can read the current Issue. + def readable_by?(user) + if user.admin? + true + elsif project.owner == user + true + elsif confidential? + author == user || + assignee == user || + project.team.member?(user, Gitlab::Access::REPORTER) + else + project.public? || + project.internal? && !user.external? || + project.team.member?(user) + end + end + + # Returns `true` if this Issue is visible to everybody. + def publicly_visible? + project.public? && !confidential? + end + def overdue? due_date.try(:past?) || false end diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 47bd6eaf35f..51b5c2b1f4c 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -1,7 +1,9 @@ class LabelLink < ActiveRecord::Base + include Importable + belongs_to :target, polymorphic: true belongs_to :label - validates :target, presence: true - validates :label, presence: true + validates :target, presence: true, unless: :importing? + validates :label, presence: true, unless: :importing? end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 04a651d50ab..865712268a0 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -25,6 +25,14 @@ class LegacyDiffNote < Note @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) end + def project_repository + if RequestStore.active? + RequestStore.fetch("project:#{project_id}:repository") { self.project.repository } + else + self.project.repository + end + end + def diff_file_hash line_code.split('_')[0] if line_code end @@ -34,7 +42,7 @@ class LegacyDiffNote < Note end def diff_file - @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff + @diff_file ||= Gitlab::Diff::File.new(diff, repository: project_repository) if diff end def diff_line diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 471e32f3b60..a99c4ba52a4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -238,11 +238,11 @@ class MergeRequest < ActiveRecord::Base end def target_branch_sha - target_branch_head.try(:sha) + @target_branch_sha || target_branch_head.try(:sha) end def source_branch_sha - source_branch_head.try(:sha) + @source_branch_sha || source_branch_head.try(:sha) end def diff_refs @@ -255,6 +255,19 @@ class MergeRequest < ActiveRecord::Base ) end + # Return diff_refs instance trying to not touch the git repository + def diff_sha_refs + if merge_request_diff && merge_request_diff.diff_refs_by_sha? + return Gitlab::Diff::DiffRefs.new( + base_sha: merge_request_diff.base_commit_sha, + start_sha: merge_request_diff.start_commit_sha, + head_sha: merge_request_diff.head_commit_sha + ) + else + diff_refs + end + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" @@ -659,7 +672,7 @@ class MergeRequest < ActiveRecord::Base end def support_new_diff_notes? - diff_refs && diff_refs.complete? + diff_sha_refs && diff_sha_refs.complete? end def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 3f520c8f3ff..119266f2d2c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -82,6 +82,10 @@ class MergeRequestDiff < ActiveRecord::Base project.commit(self.head_commit_sha) end + def diff_refs_by_sha? + base_commit_sha? && head_commit_sha? && start_commit_sha? + end + def compare @compare ||= begin diff --git a/app/models/project.rb b/app/models/project.rb index b3703d71e72..83b848ded8b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -605,6 +605,13 @@ class Project < ActiveRecord::Base web_url.split('://')[1] end + def new_issue_address(author) + if Gitlab::IncomingEmail.enabled? && author + Gitlab::IncomingEmail.reply_address( + "#{path_with_namespace}+#{author.authentication_token}") + end + end + def build_commit_note(commit) notes.new(commit_id: commit.id, noteable_type: 'Commit') end @@ -867,14 +874,6 @@ class Project < ActiveRecord::Base ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end - def developers_can_push_to_protected_branch?(branch_name) - protected_branches.matching(branch_name).any?(&:developers_can_push) - end - - def developers_can_merge_to_protected_branch?(branch_name) - protected_branches.matching(branch_name).any?(&:developers_can_merge) - end - def forked? !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end @@ -1157,7 +1156,10 @@ class Project < ActiveRecord::Base def schedule_delete!(user_id, params) # Queue this task for after the commit, so once we mark pending_delete it will run - run_after_commit { ProjectDestroyWorker.perform_async(id, user_id, params) } + run_after_commit do + job_id = ProjectDestroyWorker.perform_async(id, user_id, params) + Rails.logger.info("User #{user_id} scheduled destruction of project #{path_with_namespace} with job ID #{job_id}") + end update_attribute(:pending_delete, true) end @@ -1251,6 +1253,16 @@ class Project < ActiveRecord::Base authorized_for_user_by_shared_projects?(user, min_access_level) end + def append_or_update_attribute(name, value) + old_values = public_send(name.to_s) + + if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any? + update_attribute(name, old_values + value) + else + update_attribute(name, value) + end + end + private def authorized_for_user_by_group?(user, min_access_level) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index fdfaf052730..19fd082534c 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -138,8 +138,13 @@ class ProjectTeam def max_member_access_for_user_ids(user_ids) user_ids = user_ids.uniq key = "max_member_access:#{project.id}" - RequestStore.store[key] ||= {} - access = RequestStore.store[key] + + access = {} + + if RequestStore.active? + RequestStore.store[key] ||= {} + access = RequestStore.store[key] + end # Lookup only the IDs we need user_ids = user_ids - access.keys diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index b7011d7afdf..226b3f54342 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,6 +5,12 @@ class ProtectedBranch < ActiveRecord::Base validates :name, presence: true validates :project, presence: true + has_one :merge_access_level, dependent: :destroy + has_one :push_access_level, dependent: :destroy + + accepts_nested_attributes_for :push_access_level + accepts_nested_attributes_for :merge_access_level + def commit project.commit(self.name) end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb new file mode 100644 index 00000000000..b1112ee737d --- /dev/null +++ b/app/models/protected_branch/merge_access_level.rb @@ -0,0 +1,24 @@ +class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base + belongs_to :protected_branch + delegate :project, to: :protected_branch + + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER] } + + def self.human_access_levels + { + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters" + }.with_indifferent_access + end + + def check_access(user) + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end + + def humanize + self.class.human_access_levels[self.access_level] + end +end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb new file mode 100644 index 00000000000..6a5e49cf453 --- /dev/null +++ b/app/models/protected_branch/push_access_level.rb @@ -0,0 +1,27 @@ +class ProtectedBranch::PushAccessLevel < ActiveRecord::Base + belongs_to :protected_branch + delegate :project, to: :protected_branch + + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS] } + + def self.human_access_levels + { + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters", + Gitlab::Access::NO_ACCESS => "No one" + }.with_indifferent_access + end + + def check_access(user) + return false if access_level == Gitlab::Access::NO_ACCESS + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end + + def humanize + self.class.human_access_levels[self.access_level] + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 9fd8f5ec787..bac37483c47 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -70,7 +70,12 @@ class Repository def commit(ref = 'HEAD') return nil unless exists? - commit = Gitlab::Git::Commit.find(raw_repository, ref) + commit = + if ref.is_a?(Gitlab::Git::Commit) + ref + else + Gitlab::Git::Commit.find(raw_repository, ref) + end commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError @@ -158,7 +163,7 @@ class Repository before_remove_branch branch = find_branch(branch_name) - oldrev = branch.try(:target) + oldrev = branch.try(:target).try(:id) newrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name @@ -259,10 +264,10 @@ class Repository # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes number_commits_behind = raw_repository. - count_commits_between(branch.target, root_ref_hash) + count_commits_between(branch.target.sha, root_ref_hash) number_commits_ahead = raw_repository. - count_commits_between(root_ref_hash, branch.target) + count_commits_between(root_ref_hash, branch.target.sha) { behind: number_commits_behind, ahead: number_commits_ahead } end @@ -615,11 +620,11 @@ class Repository case value when 'name' branches.sort_by(&:name) - when 'recently_updated' + when 'updated_desc' branches.sort do |a, b| commit(b.target).committed_date <=> commit(a.target).committed_date end - when 'last_updated' + when 'updated_asc' branches.sort do |a, b| commit(a.target).committed_date <=> commit(b.target).committed_date end @@ -688,9 +693,7 @@ class Repository end def local_branches - @local_branches ||= rugged.branches.each(:local).map do |branch| - Gitlab::Git::Branch.new(branch.name, branch.target) - end + @local_branches ||= raw_repository.local_branches end alias_method :branches, :local_branches @@ -831,7 +834,7 @@ class Repository end def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id @@ -848,7 +851,7 @@ class Repository end def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id @@ -869,7 +872,7 @@ class Repository end def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha args = [commit.id, source_sha] args << { mainline: 1 } if commit.merge_commit? @@ -883,7 +886,7 @@ class Repository end def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha args = [commit.id, source_sha] args << 1 if commit.merge_commit? @@ -974,7 +977,7 @@ class Repository was_empty = empty? if !was_empty && target_branch - oldrev = target_branch.target + oldrev = target_branch.target.id end # Make commit @@ -994,7 +997,7 @@ class Repository after_create_branch else # Update head - current_head = find_branch(branch).target + current_head = find_branch(branch).target.id # Make sure target branch was not changed during pre-receive hook if current_head == oldrev @@ -1052,7 +1055,7 @@ class Repository end def tags_sorted_by_committed_date - tags.sort_by { |tag| commit(tag.target).committed_date } + tags.sort_by { |tag| tag.target.committed_date } end def keep_around_ref_name(sha) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index e294a962352..6072123b851 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -24,10 +24,14 @@ module Auth token[:access] = names.map do |name| { type: 'repository', name: name, actions: %w(*) } end - + token.encoded end + def self.token_expire_at + Time.now + current_application_settings.container_registry_token_expire_delay.minutes + end + private def authorized_token(*accesses) @@ -35,7 +39,7 @@ module Auth token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) - token.expire_time = ContainerRegistryAuthenticationService.token_expire_at + token.expire_time = self.class.token_expire_at token[:access] = accesses.compact token end @@ -81,9 +85,5 @@ module Auth def registry Gitlab.config.registry end - - def self.token_expire_at - Time.now + current_application_settings.container_registry_token_expire_delay.minutes - end end end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 332c55581a1..87f066edb6f 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -40,6 +40,6 @@ class DeleteBranchService < BaseService def build_push_data(branch) Gitlab::PushDataBuilder - .build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 1e41fbe34b6..32e0eed6b63 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -34,6 +34,6 @@ class DeleteTagService < BaseService def build_push_data(tag) Gitlab::PushDataBuilder - .build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e02b50ff9a2..3f6a177bf3a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -88,9 +88,18 @@ class GitPushService < BaseService # Set protection on the default branch if configured if current_application_settings.default_branch_protection != PROTECTION_NONE - developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false - developers_can_merge = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? true : false - @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push, developers_can_merge: developers_can_merge }) + + params = { + name: @project.default_branch, + push_access_level_attributes: { + access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + }, + merge_access_level_attributes: { + access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + } + } + + ProtectedBranches::CreateService.new(@project, current_user, params).execute end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 58573078048..969530c4fdc 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -26,8 +26,8 @@ class GitTagPushService < BaseService unless Gitlab::Git.blank_ref?(params[:newrev]) tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) - - if tag && tag.target == params[:newrev] + + if tag && tag.object_sha == params[:newrev] commit = project.commit(tag.target) commits = [commit].compact message = tag.message diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index e63e1af8766..5e2de2ccf64 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -2,10 +2,14 @@ module Issues class CreateService < Issues::BaseService def execute filter_params - label_params = params[:label_ids] - issue = project.issues.new(params.except(:label_ids)) + label_params = params.delete(:label_ids) + request = params.delete(:request) + api = params.delete(:api) + issue = project.issues.new(params) issue.author = params[:author] || current_user + issue.spam = spam_check_service.execute(request, api) + if issue.save issue.update_attributes(label_ids: label_params) notification_service.new_issue(issue, current_user) @@ -17,5 +21,11 @@ module Issues issue end + + private + + def spam_check_service + SpamCheckService.new(project, current_user, params) + end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index bc3606a14c2..ba424b09463 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -17,16 +17,19 @@ module MergeRequests end end - def hook_data(merge_request, action) + def hook_data(merge_request, action, oldrev = nil) hook_data = merge_request.to_hook_data(current_user) hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action + if oldrev && !Gitlab::Git.blank_ref?(oldrev) + hook_data[:object_attributes][:oldrev] = oldrev + end hook_data end - def execute_hooks(merge_request, action = 'open') + def execute_hooks(merge_request, action = 'open', oldrev = nil) if merge_request.project - merge_data = hook_data(merge_request, action) + merge_data = hook_data(merge_request, action, oldrev) merge_request.project.execute_hooks(merge_data, :merge_request_hooks) merge_request.project.execute_services(merge_data, :merge_request_hooks) end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 0dac0614141..b037780c431 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -35,7 +35,13 @@ module MergeRequests } commit_id = repository.merge(current_user, merge_request, options) - merge_request.update(merge_commit_sha: commit_id) + + if commit_id + merge_request.update(merge_commit_sha: commit_id) + else + merge_request.update(merge_error: 'Conflicts detected during merge') + false + end rescue GitHooksService::PreReceiveError => e merge_request.update(merge_error: e.message) false diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 1daf6bbf553..5cedd6f11d9 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -137,7 +137,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| - execute_hooks(merge_request, 'update') + execute_hooks(merge_request, 'update', @oldrev) end end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb new file mode 100644 index 00000000000..6150a2a83c9 --- /dev/null +++ b/app/services/protected_branches/create_service.rb @@ -0,0 +1,27 @@ +module ProtectedBranches + class CreateService < BaseService + attr_reader :protected_branch + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + + protected_branch = project.protected_branches.new(params) + + ProtectedBranch.transaction do + protected_branch.save! + + if protected_branch.push_access_level.blank? + protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER) + end + + if protected_branch.merge_access_level.blank? + protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER) + end + end + + protected_branch + rescue ActiveRecord::RecordInvalid + protected_branch + end + end +end diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb new file mode 100644 index 00000000000..89d8ba60134 --- /dev/null +++ b/app/services/protected_branches/update_service.rb @@ -0,0 +1,13 @@ +module ProtectedBranches + class UpdateService < BaseService + attr_reader :protected_branch + + def execute(protected_branch) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + + @protected_branch = protected_branch + @protected_branch.update(params) + @protected_branch + end + end +end diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb new file mode 100644 index 00000000000..7c3e692bde9 --- /dev/null +++ b/app/services/spam_check_service.rb @@ -0,0 +1,38 @@ +class SpamCheckService < BaseService + include Gitlab::AkismetHelper + + attr_accessor :request, :api + + def execute(request, api) + @request, @api = request, api + return false unless request || check_for_spam?(project) + return false unless is_spam?(request.env, current_user, text) + + create_spam_log + + true + end + + private + + def text + [params[:title], params[:description]].reject(&:blank?).join("\n") + end + + def spam_log_attrs + { + user_id: current_user.id, + project_id: project.id, + title: params[:title], + description: params[:description], + source_ip: client_ip(request.env), + user_agent: user_agent(request.env), + noteable_type: 'Issue', + via_api: api + } + end + + def create_spam_log + CreateSpamLogService.new(project, current_user, spam_log_attrs).execute + end +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1ab3b5789bc..e13dc9265b8 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -2,7 +2,9 @@ # # Used for creating system notes (e.g., when a user references a merge request # from an issue, an issue's assignee changes, an issue is closed, etc.) -class SystemNoteService +module SystemNoteService + extend self + # Called when commits are added to a Merge Request # # noteable - Noteable object @@ -15,7 +17,7 @@ class SystemNoteService # See new_commit_summary and existing_commit_summary. # # Returns the created Note object - def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) + def add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) total_count = new_commits.length + existing_commits.length commits_text = "#{total_count} commit".pluralize(total_count) @@ -40,7 +42,7 @@ class SystemNoteService # "Reassigned to @rspeicher" # # Returns the created Note object - def self.change_assignee(noteable, project, author, assignee) + def change_assignee(noteable, project, author, assignee) body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}" create_note(noteable: noteable, project: project, author: author, note: body) @@ -63,7 +65,7 @@ class SystemNoteService # "Removed ~5 label" # # Returns the created Note object - def self.change_label(noteable, project, author, added_labels, removed_labels) + def change_label(noteable, project, author, added_labels, removed_labels) labels_count = added_labels.count + removed_labels.count references = ->(label) { label.to_reference(format: :id) } @@ -101,7 +103,7 @@ class SystemNoteService # "Miletone changed to 7.11" # # Returns the created Note object - def self.change_milestone(noteable, project, author, milestone) + def change_milestone(noteable, project, author, milestone) body = 'Milestone ' body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}" @@ -123,7 +125,7 @@ class SystemNoteService # "Status changed to closed by bc17db76" # # Returns the created Note object - def self.change_status(noteable, project, author, status, source) + def change_status(noteable, project, author, status, source) body = "Status changed to #{status}" body << " by #{source.gfm_reference(project)}" if source @@ -131,26 +133,26 @@ class SystemNoteService end # Called when 'merge when build succeeds' is executed - def self.merge_when_build_succeeds(noteable, project, author, last_commit) + def merge_when_build_succeeds(noteable, project, author, last_commit) body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when build succeeds' is canceled - def self.cancel_merge_when_build_succeeds(noteable, project, author) + def cancel_merge_when_build_succeeds(noteable, project, author) body = 'Canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) end - def self.remove_merge_request_wip(noteable, project, author) + def remove_merge_request_wip(noteable, project, author) body = 'Unmarked this merge request as a Work In Progress' create_note(noteable: noteable, project: project, author: author, note: body) end - def self.add_merge_request_wip(noteable, project, author) + def add_merge_request_wip(noteable, project, author) body = 'Marked this merge request as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) @@ -168,7 +170,7 @@ class SystemNoteService # "Title changed from **Old** to **New**" # # Returns the created Note object - def self.change_title(noteable, project, author, old_title) + def change_title(noteable, project, author, old_title) new_title = noteable.title.dup old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs @@ -191,7 +193,7 @@ class SystemNoteService # "Made the issue confidential" # # Returns the created Note object - def self.change_issue_confidentiality(issue, project, author) + def change_issue_confidentiality(issue, project, author) body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' create_note(noteable: issue, project: project, author: author, note: body) end @@ -210,7 +212,7 @@ class SystemNoteService # "Target branch changed from `Old` to `New`" # # Returns the created Note object - def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + def change_branch(noteable, project, author, branch_type, old_branch, new_branch) body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -229,7 +231,7 @@ class SystemNoteService # "Restored target branch `feature`" # # Returns the created Note object - def self.change_branch_presence(noteable, project, author, branch_type, branch, presence) + def change_branch_presence(noteable, project, author, branch_type, branch, presence) verb = if presence == :add 'restored' @@ -245,7 +247,7 @@ class SystemNoteService # Example note text: # # "Started branch `201-issue-branch-button`" - def self.new_issue_branch(issue, project, author, branch) + def new_issue_branch(issue, project, author, branch) h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) @@ -270,7 +272,7 @@ class SystemNoteService # See cross_reference_note_content. # # Returns the created Note object - def self.cross_reference(noteable, mentioner, author) + def cross_reference(noteable, mentioner, author) return if cross_reference_disallowed?(noteable, mentioner) gfm_reference = mentioner.gfm_reference(noteable.project) @@ -294,7 +296,7 @@ class SystemNoteService end end - def self.cross_reference?(note_text) + def cross_reference?(note_text) note_text.start_with?(cross_reference_note_prefix) end @@ -308,7 +310,7 @@ class SystemNoteService # mentioner - Mentionable object # # Returns Boolean - def self.cross_reference_disallowed?(noteable, mentioner) + def cross_reference_disallowed?(noteable, mentioner) return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) @@ -328,7 +330,7 @@ class SystemNoteService # # Returns Boolean - def self.cross_reference_exists?(noteable, mentioner) + def cross_reference_exists?(noteable, mentioner) # Initial scope should be system notes of this noteable type notes = Note.system.where(noteable_type: noteable.class) @@ -342,9 +344,60 @@ class SystemNoteService notes_for_mentioner(mentioner, noteable, notes).count > 0 end + # Build an Array of lines detailing each commit added in a merge request + # + # new_commits - Array of new Commit objects + # + # Returns an Array of Strings + def new_commit_summary(new_commits) + new_commits.collect do |commit| + "* #{commit.short_id} - #{escape_html(commit.title)}" + end + end + + # Called when the status of a Task has changed + # + # noteable - Noteable object. + # project - Project owning noteable + # author - User performing the change + # new_task - TaskList::Item object. + # + # Example Note text: + # + # "Soandso marked the task Whatever as completed." + # + # Returns the created Note object + def change_task_status(noteable, project, author, new_task) + status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE + body = "Marked the task **#{new_task.source}** as #{status_label}" + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when noteable has been moved to another project + # + # direction - symbol, :to or :from + # noteable - Noteable object + # noteable_ref - Referenced noteable + # author - User performing the move + # + # Example Note text: + # + # "Moved to some_namespace/project_new#11" + # + # Returns the created Note object + def noteable_moved(noteable, project, noteable_ref, author, direction:) + unless [:to, :from].include?(direction) + raise ArgumentError, "Invalid direction `#{direction}`" + end + + cross_reference = noteable_ref.to_reference(project) + body = "Moved #{direction} #{cross_reference}" + create_note(noteable: noteable, project: project, author: author, note: body) + end + private - def self.notes_for_mentioner(mentioner, noteable, notes) + def notes_for_mentioner(mentioner, noteable, notes) if mentioner.is_a?(Commit) notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}") else @@ -353,29 +406,18 @@ class SystemNoteService end end - def self.create_note(args = {}) + def create_note(args = {}) Note.create(args.merge(system: true)) end - def self.cross_reference_note_prefix + def cross_reference_note_prefix 'mentioned in ' end - def self.cross_reference_note_content(gfm_reference) + def cross_reference_note_content(gfm_reference) "#{cross_reference_note_prefix}#{gfm_reference}" end - # Build an Array of lines detailing each commit added in a merge request - # - # new_commits - Array of new Commit objects - # - # Returns an Array of Strings - def self.new_commit_summary(new_commits) - new_commits.collect do |commit| - "* #{commit.short_id} - #{escape_html(commit.title)}" - end - end - # Build a single line summarizing existing commits being added in a merge # request # @@ -392,7 +434,7 @@ class SystemNoteService # "* ea0f8418 - 1 commit from branch `feature`" # # Returns a newline-terminated String - def self.existing_commit_summary(noteable, existing_commits, oldrev = nil) + def existing_commit_summary(noteable, existing_commits, oldrev = nil) return '' if existing_commits.empty? count = existing_commits.size @@ -415,47 +457,7 @@ class SystemNoteService "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" end - # Called when the status of a Task has changed - # - # noteable - Noteable object. - # project - Project owning noteable - # author - User performing the change - # new_task - TaskList::Item object. - # - # Example Note text: - # - # "Soandso marked the task Whatever as completed." - # - # Returns the created Note object - def self.change_task_status(noteable, project, author, new_task) - status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE - body = "Marked the task **#{new_task.source}** as #{status_label}" - create_note(noteable: noteable, project: project, author: author, note: body) - end - - # Called when noteable has been moved to another project - # - # direction - symbol, :to or :from - # noteable - Noteable object - # noteable_ref - Referenced noteable - # author - User performing the move - # - # Example Note text: - # - # "Moved to some_namespace/project_new#11" - # - # Returns the created Note object - def self.noteable_moved(noteable, project, noteable_ref, author, direction:) - unless [:to, :from].include?(direction) - raise ArgumentError, "Invalid direction `#{direction}`" - end - - cross_reference = noteable_ref.to_reference(project) - body = "Moved #{direction} #{cross_reference}" - create_note(noteable: noteable, project: project, author: author, note: body) - end - - def self.escape_html(text) + def escape_html(text) Rack::Utils.escape_html(text) end end diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index ce818c30c30..352adbedee4 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -11,16 +11,18 @@ - else %span.build-link ##{build.id} - - if build.stuck? - %i.fa.fa-warning.text-warning - - if build.ref + .icon-container + = build.tag? ? icon('tag') : icon('code-fork') = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none - = custom_icon("icon_commit") + .icon-container + = custom_icon("icon_commit") = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id" + - if build.stuck? + %i.fa.fa-warning.text-warning .label-container - if build.tags.any? diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 431d312b4ca..85e188d6f8b 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ %li %a Sort by date - = link_to 'New issue', '#', class: 'btn btn-new' + = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' .lead Only nav links without button and search diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index cdac50f7a8d..ff893ea74e1 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -16,6 +16,7 @@ - if current_user .btn-group{ role: "group" } - = edit_blob_link + - if blob_text_viewable?(@blob) + = edit_blob_link = replace_blob_link = delete_blob_link diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index 9fe65cbb104..d54c76ff9c8 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -1,5 +1,5 @@ .branch-commit - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace" · %span.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index cb190d121c2..e889f29c816 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -14,18 +14,15 @@ .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light - - if params[:sort].present? - = params[:sort].humanize - - else - Name + = projects_sort_options_hash[@sort] %b.caret %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to filter_branches_path(sort: nil) do + = link_to filter_branches_path(sort: sort_value_name) do = sort_title_name - = link_to filter_branches_path(sort: 'recently_updated') do + = link_to filter_branches_path(sort: sort_value_recently_updated) do = sort_title_recently_updated - = link_to filter_branches_path(sort: 'last_updated') do + = link_to filter_branches_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - if can? current_user, :push_code, @project diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index a3114771a42..91081435220 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,13 +13,6 @@ - else %span ##{build.id} - - if build.stuck? - .icon-container - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - - if defined?(retried) && retried - .icon-container - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - - if defined?(ref) && ref - if build.ref .icon-container @@ -33,6 +26,11 @@ - if defined?(commit_sha) && commit_sha = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + - if build.stuck? + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + .label-container - if build.tags.any? - build.tags.each do |tag| @@ -47,7 +45,6 @@ - if build.manual? %span.label.label-info manual - - if defined?(runner) && runner %td - if build.try(:runner) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 2f7d54f0bdd..558c35553da 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -57,7 +57,7 @@ %td.pipeline-actions .controls.hidden-xs.pull-right - - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } + - artifacts = pipeline.builds.latest.with_artifacts_not_expired - actions = pipeline.manual_actions - if artifacts.present? || actions.any? .btn-group.inline diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c306909fb1a..1854c64cbd7 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -9,10 +9,11 @@ = icon('comment') \ - - if editable_diff?(diff_file) - = edit_blob_link(@merge_request.source_project, - @merge_request.source_branch, diff_file.new_path, - from_merge_request_id: @merge_request.id) + - if editable_diff?(diff_file) + = edit_blob_link(@merge_request.source_project, + @merge_request.source_branch, diff_file.new_path, + from_merge_request_id: @merge_request.id, + skip_visible_check: true) = view_file_btn(diff_commit.id, diff_file, project) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 5a8a131d10c..4d3af905b58 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,8 +1,7 @@ - plain = local_assigns.fetch(:plain, false) -- line_code = diff_file.line_code(line) -- position = diff_file.position(line) - type = line.type -%tr.line_holder{ id: line_code, class: type } +- line_code = diff_file.line_code(line) unless plain +%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } - case type - when 'match' = render "projects/diffs/match_line", { line: line.text, @@ -24,4 +23,4 @@ = link_text - else %a{href: "##{line_code}", data: { linenumber: link_text }} - %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, position, type) unless plain) }= diff_line_content(line.text, type) + %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type) diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index ea2a3e01277..e751dabdf99 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -2,7 +2,7 @@ .commit-stat-summary Showing = link_to '#', class: 'js-toggle-button' do - %strong #{pluralize(diff_files.count, "changed file")} + %strong #{pluralize(diff_files.size, "changed file")} with %strong.cgreen #{diff_files.sum(&:added_lines)} additions and diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 10fa1ddf2e5..295a1b62535 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -11,5 +11,5 @@ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" %p To preserve performance only - %strong #{diff_files.count} of #{diff_files.real_size} + %strong #{diff_files.size} of #{diff_files.real_size} files are displayed. diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml index c07f4bd510c..6d040f5cfe6 100644 --- a/app/views/projects/environments/_form.html.haml +++ b/app/views/projects/environments/_form.html.haml @@ -1,7 +1,22 @@ -= form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f| - = form_errors(@environment) - .form-group - = f.label :name, 'Name', class: 'label-light' - = f.text_field :name, required: true, class: 'form-control' - = f.submit 'Create environment', class: 'btn btn-create' - = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Environments + %p + Environments allow you to track deployments of your application + = succeed "." do + = link_to "Read more about environments", help_page_path("ci/environments") + + = form_for [@project.namespace.becomes(Namespace), @project, @environment], html: { class: 'col-lg-9' } do |f| + = form_errors(@environment) + + .form-group + = f.label :name, 'Name', class: 'label-light' + = f.text_field :name, required: true, class: 'form-control' + .form-group + = f.label :external_url, 'External URL', class: 'label-light' + = f.url_field :external_url, class: 'form-control' + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml new file mode 100644 index 00000000000..6d1bdb9320f --- /dev/null +++ b/app/views/projects/environments/edit.html.haml @@ -0,0 +1,6 @@ +- page_title "Edit", @environment.name, "Environments" + +%h3.page-title + Edit environment +%hr += render 'form' diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 89e06567196..e51667ade2d 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,12 +1,6 @@ - page_title 'New Environment' -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 - New Environment - %p - Environments allow you to track deployments of your application - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - = render 'form' +%h3.page-title + New environment +%hr += render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index b8b1ce52a91..a07436ad7c9 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -6,10 +6,10 @@ .top-area .col-md-9 %h3.page-title= @environment.name.capitalize - .col-md-3 .nav-controls - if can?(current_user, :update_environment, @environment) + = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - if @deployments.blank? diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml new file mode 100644 index 00000000000..72669372497 --- /dev/null +++ b/app/views/projects/issues/_issue_by_email.html.haml @@ -0,0 +1,27 @@ +.issues-footer.text-center + %button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } } + Email a new issue to this project + +#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" } + .modal-dialog{ role: "document" } + .modal-content + .modal-header + %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } } + %span{ aria: { hidden: "true" } }= icon("times") + %h4.modal-title + Create new issue by email + .modal-body + %p + Write an email to the below email address. (This is a private email address, so keep it secret.) + .email-modal-input-group.input-group + = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true + .input-group-btn + = clipboard_button(clipboard_target: '#issue_email') + %p + Send an email to this address to create an issue. + %p + Use the subject line as the title of your issue. + %p + Use the message as the body of your issue (feel free to include some nice + = succeed ")." do + = link_to "Markdown", help_page_path('markdown', 'markdown') diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index c6fc499a7b8..6ea9f612d13 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -4,8 +4,8 @@ %ul.unstyled-list - @related_branches.each do |branch| %li - - sha = @project.repository.find_branch(branch).target - - pipeline = @project.pipeline(sha, branch) if sha + - target = @project.repository.find_branch(branch).target + - pipeline = @project.pipeline(target.sha, branch) if target - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7612fe3719a..1a87045aa60 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,5 +1,6 @@ - @no_container = true - page_title "Issues" +- new_issue_email = @project.new_issue_address(current_user) = render "projects/issues/head" = content_for :meta_tags do @@ -18,12 +19,20 @@ Subscribe = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New Issue", + id: "new_issue_link" do New Issue = render 'shared/issuable/filter', type: :issues .issues-holder - = render "issues" + = render 'issues' + - if new_issue_email + = render 'issue_by_email', email: new_issue_email - else .blank-state.blank-state-welcome %h2.blank-state-title.blank-state-welcome-title @@ -40,3 +49,5 @@ - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do New Issue + - if new_issue_email + = render 'issue_by_email', email: new_issue_email diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 9b6a97c0959..e5cce16a171 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -38,7 +38,7 @@ %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 720d67dff7c..0603a014008 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -5,24 +5,22 @@ No branches are protected, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) - .table-responsive - %table.table.protected-branches-list - %colgroup - %col{ width: "20%" } - %col{ width: "30%" } - %col{ width: "25%" } - %col{ width: "25%" } + + %table.table.protected-branches-list + %colgroup + %col{ width: "20%" } + %col{ width: "30%" } + %col{ width: "25%" } + %col{ width: "25%" } + %thead + %tr + %th Branch + %th Last commit + %th Allowed to merge + %th Allowed to push - if can_admin_project - %col - %thead - %tr - %th Protected Branch - %th Commit - %th Developers Can Push - %th Developers Can Merge - - if can_admin_project - %th - %tbody - = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + %th + %tbody + = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 7fda7f96047..498e412235e 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -15,9 +15,15 @@ - else (branch was removed from repository) %td - = check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url }) + = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level + = dropdown_tag(protected_branch.merge_access_level.humanize, + options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', + data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }}) %td - = check_box_tag("developers_can_merge", protected_branch.id, protected_branch.developers_can_merge, data: { url: url }) + = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level + = dropdown_tag(protected_branch.push_access_level.humanize, + options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', + data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }}) - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 950df740bbc..4efe44c7233 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -32,18 +32,22 @@ are supported. .form-group - = f.check_box :developers_can_push, class: "pull-left" - .prepend-left-20 - = f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0" - %p.light.append-bottom-0 - Allow developers to push to this branch + = hidden_field_tag 'protected_branch[merge_access_level_attributes][access_level]' + = label_tag "Allowed to merge: ", nil, class: "label-light append-bottom-0" + = dropdown_tag("<Make a selection>", + options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "protected_branch[merge_access_level_attributes][access_level]" }}) .form-group - = f.check_box :developers_can_merge, class: "pull-left" - .prepend-left-20 - = f.label :developers_can_merge, "Developers can merge", class: "label-light append-bottom-0" - %p.light.append-bottom-0 - Allow developers to accept merge requests to this branch + = hidden_field_tag 'protected_branch[push_access_level_attributes][access_level]' + = label_tag "Allowed to push: ", nil, class: "label-light append-bottom-0" + = dropdown_tag("<Make a selection>", + options: { title: "Allowed to push", toggle_class: 'allowed-to-push', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "protected_branch[push_access_level_attributes][access_level]" }}) + + = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true %hr diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index dd1cf680cfa..a666d07e9eb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -43,6 +43,10 @@ %li = link_to 'Contribution guide', contribution_guide_path(@project) + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', ci_configuration_path(@project) + - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 797a1a59e9f..643f7c589e6 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -18,9 +18,14 @@ .error-alert .help-block - To link to a (new) page, simply type - %code [Link Title](page-slug) - \. + = succeed '.' do + To link to a (new) page, simply type + %code [Link Title](page-slug) + + = succeed '.' do + More examples are in the + = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown") + .form-group = f.label :commit_message, class: 'control-label' .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 8163aff43b6..e0400083870 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,6 +1,7 @@ - project = note.project - note_url = Gitlab::UrlBuilder.build(note) -- noteable_identifier = note.noteable.try(:iid) || note.noteable.id +- noteable_identifier = note.noteable.try(:iid) || note.noteable.try(:id) + .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment @@ -10,7 +11,10 @@ · - if note.for_commit? - = link_to "Commit #{truncate_sha(note.commit_id)}", note_url + = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do + = truncate_sha(note.commit_id) + %span.light Commit deleted + - else %span #{note.noteable_type.titleize} ##{noteable_identifier} · diff --git a/app/views/shared/icons/_icon_status_cancel.svg b/app/views/shared/icons/_icon_status_cancel.svg index 6a0bc1490c4..fd1ebbcbabd 100644 --- a/app/views/shared/icons/_icon_status_cancel.svg +++ b/app/views/shared/icons/_icon_status_cancel.svg @@ -1,12 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <circle id="a" cx="7" cy="7" r="7"/> - <mask id="b" width="14" height="14" x="0" y="0" fill="white"> - <use xlink:href="#a"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd"> - <use stroke="#5C5C5C" stroke-width="2" mask="url(#b)" xlink:href="#a"/> - <rect width="10" height="1" x="2" y="6.5" fill="#5C5C5C" transform="rotate(45 7 7)" rx=".3"/> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <g fill="#5C5C5C" fill-rule="evenodd"> + <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> + <rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/> </g> </svg> diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg index c41ca18cae7..e56e0887416 100644 --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1,12 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <circle id="a" cx="7" cy="7" r="7"/> - <mask id="b" width="14" height="14" x="0" y="0" fill="white"> - <use xlink:href="#a"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd"> - <use stroke="#D22852" stroke-width="2" mask="url(#b)" xlink:href="#a"/> - <path fill="#D22852" d="M7.5,6.5 L7.5,4.30578971 C7.5,4.12531853 7.36809219,4 7.20537567,4 L6.79462433,4 C6.63904572,4 6.5,4.13690672 6.5,4.30578971 L6.5,6.5 L4.30578971,6.5 C4.12531853,6.5 4,6.63190781 4,6.79462433 L4,7.20537567 C4,7.36095428 4.13690672,7.5 4.30578971,7.5 L6.5,7.5 L6.5,9.69421029 C6.5,9.87468147 6.63190781,10 6.79462433,10 L7.20537567,10 C7.36095428,10 7.5,9.86309328 7.5,9.69421029 L7.5,7.5 L9.69421029,7.5 C9.87468147,7.5 10,7.36809219 10,7.20537567 L10,6.79462433 C10,6.63904572 9.86309328,6.5 9.69421029,6.5 L7.5,6.5 Z" transform="rotate(45 7 7)"/> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <g fill="#D22852" fill-rule="evenodd"> + <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> + <path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/> </g> </svg> diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg index 035cd8b4ccc..117f0367161 100644 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1,13 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <circle id="a" cx="7" cy="7" r="7"/> - <mask id="b" width="14" height="14" x="0" y="0" fill="white"> - <use xlink:href="#a"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd"> - <use stroke="#E75E40" stroke-width="2" mask="url(#b)" xlink:href="#a"/> - <rect width="1" height="4" x="5" y="5" fill="#E75E40" rx=".3"/> - <rect width="1" height="4" x="8" y="5" fill="#E75E40" rx=".3"/> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <g fill="#E75E40" fill-rule="evenodd"> + <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> + <path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/> </g> </svg> diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg index a48b3a25099..920d7952eb5 100644 --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1,12 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <circle id="a" cx="7" cy="7" r="7"/> - <mask id="b" width="14" height="14" x="0" y="0" fill="white"> - <use xlink:href="#a"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd"> - <use stroke="#2D9FD8" stroke-width="2" mask="url(#b)" xlink:href="#a"/> - <path fill="#2D9FD8" d="M7,3.00800862 C9.09023405,3.13960661 10.7448145,4.87657932 10.7448145,7 C10.7448145,9.209139 8.95395346,11 6.74481446,11 C5.4560962,11 4.30972054,10.3905589 3.57817301,9.44416214 L7,7 L7,3.00800862 Z"/> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <g fill="#2D9FD8" fill-rule="evenodd"> + <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> + <path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/> </g> </svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 260eab013a3..67b378b3571 100644 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1,15 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <circle id="a" cx="7" cy="7" r="7"/> - <mask id="b" width="14" height="14" x="0" y="0" fill="white"> - <use xlink:href="#a"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd"> - <use stroke="#31AF64" stroke-width="2" mask="url(#b)" xlink:href="#a"/> - <g fill="#31AF64" transform="rotate(45 -.13 10.953)"> - <rect width="1" height="5" x="2" rx=".3"/> - <rect width="3" height="1" y="4" rx=".3"/> - </g> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <g fill="#31AF64" fill-rule="evenodd"> + <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> + <path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/> </g> </svg> diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg index d47e7a1c93f..d0ad4bd65b1 100644 --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1,15 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <circle id="a" cx="7" cy="7" r="7"/> - <mask id="b" width="14" height="14" x="0" y="0" fill="white"> - <use xlink:href="#a"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd"> - <g fill="#FF8A24" transform="translate(6 3)"> - <rect width="2" height="5" rx=".5"/> - <rect width="2" height="2" y="6" rx=".5"/> - </g> - <use stroke="#FF8A24" stroke-width="2" mask="url(#b)" xlink:href="#a"/> +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> + <g fill="#FF8A24" fill-rule="evenodd"> + <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> + <path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/> </g> </svg> diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index f2649e38eb3..842eebdea9e 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -21,31 +21,35 @@ class EmailReceiverWorker return unless raw.present? can_retry = false - reason = nil - - case e - when Gitlab::Email::Receiver::SentNotificationNotFoundError - reason = "We couldn't figure out what the email is in reply to. Please create your comment through the web interface." - when Gitlab::Email::Receiver::EmptyEmailError - can_retry = true - reason = "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." - when Gitlab::Email::Receiver::AutoGeneratedEmailError - reason = "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." - when Gitlab::Email::Receiver::UserNotFoundError - reason = "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." - when Gitlab::Email::Receiver::UserBlockedError - reason = "Your account has been blocked. If you believe this is in error, contact a staff member." - when Gitlab::Email::Receiver::UserNotAuthorizedError - reason = "You are not allowed to respond to the thread you are replying to. If you believe this is in error, contact a staff member." - when Gitlab::Email::Receiver::NoteableNotFoundError - reason = "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member." - when Gitlab::Email::Receiver::InvalidNoteError - can_retry = true - reason = e.message - else - return + reason = + case e + when Gitlab::Email::UnknownIncomingEmail + "We couldn't figure out what the email is for. Please create your issue or comment through the web interface." + when Gitlab::Email::SentNotificationNotFoundError + "We couldn't figure out what the email is in reply to. Please create your comment through the web interface." + when Gitlab::Email::ProjectNotFound + "We couldn't find the project. Please check if there's any typo." + when Gitlab::Email::EmptyEmailError + can_retry = true + "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." + when Gitlab::Email::AutoGeneratedEmailError + "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." + when Gitlab::Email::UserNotFoundError + "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." + when Gitlab::Email::UserBlockedError + "Your account has been blocked. If you believe this is in error, contact a staff member." + when Gitlab::Email::UserNotAuthorizedError + "You are not allowed to perform this action. If you believe this is in error, contact a staff member." + when Gitlab::Email::NoteableNotFoundError + "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member." + when Gitlab::Email::InvalidNoteError, + Gitlab::Email::InvalidIssueError + can_retry = true + e.message + end + + if reason + EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later end - - EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later end end diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index f3cddac5b36..b68a09ce730 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -144,6 +144,7 @@ if Gitlab::Metrics.enabled? end config.instrument_methods(Rinku) + config.instrument_instance_methods(Repository) end GC::Profiler.enable diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb index fb5a7b8372e..a9aa802681a 100644 --- a/config/initializers/request_profiler.rb +++ b/config/initializers/request_profiler.rb @@ -1,3 +1,5 @@ +require 'gitlab/request_profiler/middleware' + Rails.application.configure do |config| config.middleware.use(Gitlab::RequestProfiler::Middleware) end diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb index 30770b71e24..cd869657c53 100644 --- a/config/initializers/trusted_proxies.rb +++ b/config/initializers/trusted_proxies.rb @@ -7,6 +7,8 @@ module Rack class Request def trusted_proxy?(ip) Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip } + rescue IPAddr::InvalidAddressError + false end end end diff --git a/config/routes.rb b/config/routes.rb index 414ba69dfae..371eb4bee7f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -279,7 +279,7 @@ Rails.application.routes.draw do resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] - resources :requests_profiles, only: [:index, :show], param: :name + resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do root to: 'projects#index', as: :projects @@ -741,7 +741,7 @@ Rails.application.routes.draw do end end - resources :environments, only: [:index, :show, :new, :create, :destroy] + resources :environments resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index b4639999967..e3316ecdb6c 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -20,7 +20,6 @@ Sidekiq::Testing.inline! do 'https://github.com/airbnb/javascript.git', 'https://github.com/tessalt/echo-chamber-js.git', 'https://github.com/atom/atom.git', - 'https://github.com/ipselon/react-ui-builder.git', 'https://github.com/mattermost/platform.git', 'https://github.com/purifycss/purifycss.git', 'https://github.com/facebook/nuclide.git', diff --git a/db/fixtures/development/16_protected_branches.rb b/db/fixtures/development/16_protected_branches.rb new file mode 100644 index 00000000000..103c7f9445c --- /dev/null +++ b/db/fixtures/development/16_protected_branches.rb @@ -0,0 +1,12 @@ +Gitlab::Seeder.quiet do + admin_user = User.find(1) + + Project.all.each do |project| + params = { + name: 'master' + } + + ProtectedBranches::CreateService.new(project, admin_user, params).execute + print '.' + end +end diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb new file mode 100644 index 00000000000..f27295524e1 --- /dev/null +++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb @@ -0,0 +1,17 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddProtectedBranchesPushAccess < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :protected_branch_push_access_levels do |t| + t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false + + # Gitlab::Access::MASTER == 40 + t.integer :access_level, default: 40, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb new file mode 100644 index 00000000000..32adfa266cd --- /dev/null +++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb @@ -0,0 +1,17 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddProtectedBranchesMergeAccess < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :protected_branch_merge_access_levels do |t| + t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false + + # Gitlab::Access::MASTER == 40 + t.integer :access_level, default: 40, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb new file mode 100644 index 00000000000..fa93936ced7 --- /dev/null +++ b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::Migration + DOWNTIME = true + DOWNTIME_REASON = <<-HEREDOC + We're creating a `merge_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this + is running, we might be left with a `protected_branch` _without_ an associated `merge_access_level`. The `protected_branches` + table must not change while this is running, so downtime is required. + + https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410 + HEREDOC + + def up + execute <<-HEREDOC + INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at) + SELECT id, (CASE WHEN developers_can_merge THEN 1 ELSE 0 END), now(), now() + FROM protected_branches + HEREDOC + end + + def down + execute <<-HEREDOC + UPDATE protected_branches SET developers_can_merge = TRUE + WHERE id IN (SELECT protected_branch_id FROM protected_branch_merge_access_levels + WHERE access_level = 1); + HEREDOC + end +end diff --git a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb new file mode 100644 index 00000000000..56f6159d1d8 --- /dev/null +++ b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Migration + DOWNTIME = true + DOWNTIME_REASON = <<-HEREDOC + We're creating a `push_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this + is running, we might be left with a `protected_branch` _without_ an associated `push_access_level`. The `protected_branches` + table must not change while this is running, so downtime is required. + + https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410 + HEREDOC + + def up + execute <<-HEREDOC + INSERT into protected_branch_push_access_levels (protected_branch_id, access_level, created_at, updated_at) + SELECT id, (CASE WHEN developers_can_push THEN 1 ELSE 0 END), now(), now() + FROM protected_branches + HEREDOC + end + + def down + execute <<-HEREDOC + UPDATE protected_branches SET developers_can_push = TRUE + WHERE id IN (SELECT protected_branch_id FROM protected_branch_push_access_levels + WHERE access_level = 1); + HEREDOC + end +end diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb new file mode 100644 index 00000000000..f563f660ddf --- /dev/null +++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDevelopersCanPushFromProtectedBranches < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # This is only required for `#down` + disable_ddl_transaction! + + DOWNTIME = false + + def up + remove_column :protected_branches, :developers_can_push, :boolean + end + + def down + add_column_with_default(:protected_branches, :developers_can_push, :boolean, default: false, null: false) + end +end diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb new file mode 100644 index 00000000000..aa71e06d36e --- /dev/null +++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDevelopersCanMergeFromProtectedBranches < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # This is only required for `#down` + disable_ddl_transaction! + + DOWNTIME = false + + def up + remove_column :protected_branches, :developers_can_merge, :boolean + end + + def down + add_column_with_default(:protected_branches, :developers_can_merge, :boolean, default: false, null: false) + end +end diff --git a/db/migrate/20160725083350_add_external_url_to_enviroments.rb b/db/migrate/20160725083350_add_external_url_to_enviroments.rb new file mode 100644 index 00000000000..21a8abd310b --- /dev/null +++ b/db/migrate/20160725083350_add_external_url_to_enviroments.rb @@ -0,0 +1,9 @@ +class AddExternalUrlToEnviroments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:environments, :external_url, :string) + end +end diff --git a/db/schema.rb b/db/schema.rb index 15cee55a7bf..5b35a528e71 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160722221922) do +ActiveRecord::Schema.define(version: 20160726093600) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -427,9 +427,10 @@ ActiveRecord::Schema.define(version: 20160722221922) do create_table "environments", force: :cascade do |t| t.integer "project_id" - t.string "name", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" + t.string "external_url" end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree @@ -867,13 +868,29 @@ ActiveRecord::Schema.define(version: 20160722221922) do add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree + create_table "protected_branch_merge_access_levels", force: :cascade do |t| + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 40, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree + + create_table "protected_branch_push_access_levels", force: :cascade do |t| + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 40, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree + create_table "protected_branches", force: :cascade do |t| - t.integer "project_id", null: false - t.string "name", null: false + t.integer "project_id", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "developers_can_push", default: false, null: false - t.boolean "developers_can_merge", default: false, null: false end add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree @@ -1136,5 +1153,7 @@ ActiveRecord::Schema.define(version: 20160722221922) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "personal_access_tokens", "users" + add_foreign_key "protected_branch_merge_access_levels", "protected_branches" + add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "u2f_registrations", "users" end diff --git a/doc/README.md b/doc/README.md index b5b377822e6..751e685b19b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -9,7 +9,7 @@ - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). - [Importing and exporting projects between instances](user/project/settings/import_export.md). -- [Markdown](markdown/markdown.md) GitLab's advanced formatting system. +- [Markdown](user/project/markdown.md) GitLab's advanced formatting system. - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) diff --git a/doc/api/commits.md b/doc/api/commits.md index 57c2e1d9b87..2960c2ae428 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -81,6 +81,11 @@ Example response: "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], + "stats": { + "additions": 15, + "deletions": 10, + "total": 25 + }, "status": "running" } ``` diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md new file mode 100644 index 00000000000..1e12ded448c --- /dev/null +++ b/doc/api/enviroments.md @@ -0,0 +1,117 @@ +# Environments + +## List environments + +Get all environments for a given project. + +``` +GET /projects/:id/environments +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "Env1", + "external_url": "https://env1.example.gitlab.com" + } +] +``` + +## Create a new environment + +Creates a new environment with the given name and external_url. + +It returns 201 if the environment was successfully created, 400 for wrong parameters. + +``` +POST /projects/:id/environment +``` + +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the environment | +| `external_url` | string | no | Place to link to for this environment | + +```bash +curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments" +``` + +Example response: + +```json +{ + "id": 1, + "name": "deploy", + "external_url": "https://deploy.example.gitlab.com" +} +``` + +## Edit an existing environment + +Updates an existing environment's name and/or external_url. + +It returns 200 if the environment was successfully updated. In case of an error, a status code 400 is returned. + +``` +PUT /projects/:id/environments/:environments_id +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer | yes | The ID of the project | +| `environment_id` | integer | yes | The ID of the environment | The ID of the environment | +| `name` | string | no | The new name of the environment | +| `external_url` | string | no | The new external_url | + +```bash +curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "staging", + "external_url": "https://staging.example.gitlab.com" +} +``` + +## Delete an environment + +It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist. + +``` +DELETE /projects/:id/environments/:environment_id +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | +| `environment_id` | integer | yes | The ID of the environment | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "deploy", + "external_url": "https://deploy.example.gitlab.com" +} +``` diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 31902e145f6..7ce89adc98b 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -35,7 +35,7 @@ Where REDIRECT_URI is the URL in your app where users will be sent after authori To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client: ``` -parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI' +parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' RestClient.post 'http://localhost:3000/oauth/token', parameters # The response will be diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 7fa1a478f34..6a3c416d995 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -233,7 +233,7 @@ Awesome! You started using CI in GitLab! Visit the [examples README][examples] to see a list of examples using GitLab CI with various languages. -[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation +[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ [examples]: ../examples/README.md [ci]: https://about.gitlab.com/gitlab-ci/ diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ea3fff1596e..01d71088543 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -379,6 +379,8 @@ job: - bundle exec rspec ``` +Sometimes, `script` commands will need to be wrapped in single or double quotes. For example, commands that contain a colon (`:`) need to be wrapped in quotes so that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters (`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``). + ### stage `stage` allows to group build into different stages. Builds of the same `stage` diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 6ee7b3cfeeb..3a3597bccaa 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -244,6 +244,12 @@ In this case: Here is a list of must-have items. Use them in the exact order that appears on this document. Further explanation is given below. +- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods) + (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb + for a good example): + - `desc` for the method summary (you can pass it a block for additional details) + - `params` for the method params (this acts as description **and** validation + of the params) - Every method must have the REST API request. For example: ``` diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 89ce8bcc3e8..b61f436c1a4 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -120,3 +120,11 @@ You need to be in the created branch. git checkout NAME-OF-BRANCH git merge master ``` + +### Merge master branch with created branch +You need to be in the master branch. +``` +git checkout master +git merge NAME-OF-BRANCH +``` + diff --git a/doc/install/installation.md b/doc/install/installation.md index 9bc0dbb5e2a..af8e31a705b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -269,9 +269,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-10-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-11-stable gitlab -**Note:** You can change `8-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index 5cc09bd536d..c222d21612f 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -1,9 +1,14 @@ # Akismet +> *Note:* Before 8.11 only issues submitted via the API and for non-project +members were submitted to Akismet. + GitLab leverages [Akismet](http://akismet.com) to protect against spam. Currently -GitLab uses Akismet to prevent users who are not members of a project from -creating spam via the GitLab API. Detected spam will be rejected, and -an entry in the "Spam Log" section in the Admin page will be created. +GitLab uses Akismet to prevent the creation of spam issues on public projects. Issues +created via the WebUI or the API can be submitted to Akismet for review. + +Detected spam will be rejected, and an entry in the "Spam Log" section in the +Admin page will be created. Privacy note: GitLab submits the user's IP and user agent to Akismet. Note that adding a user to a project will disable the Akismet check and prevent this diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index fa976134341..5fa96736d59 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -382,6 +382,13 @@ backups using all your disk space. To do this add the following lines to gitlab_rails['backup_keep_time'] = 604800 ``` +Note that the `backup_keep_time` configuration option only manages local +files. GitLab does not automatically prune old files stored in a third-party +object storage (e.g. AWS S3) because the user may not have permission to list +and delete files. We recommend that you configure the appropriate retention +policy for your object storage. For example, you can configure [the S3 backup +policy here as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3). + NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). ## Alternative backup strategies diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md new file mode 100644 index 00000000000..25343d484ba --- /dev/null +++ b/doc/update/8.10-to-8.11.md @@ -0,0 +1,167 @@ +# From 8.10 to 8.11 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-11-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-11-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v3.2.1 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.8 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-10-stable:config/gitlab.yml.example origin/8-11-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-10-stable:lib/support/nginx/gitlab-ssl origin/8-11-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-10-stable:lib/support/nginx/gitlab origin/8-11-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-11-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-11-stable/config/initializers/smtp_settings.rb.sample#L13? + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.10) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.9 to 8.10](8.9-to-8.10.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/markdown/img/logo.png b/doc/user/project/img/markdown_logo.png Binary files differindex 05c8b0d0ccf..05c8b0d0ccf 100644 --- a/doc/markdown/img/logo.png +++ b/doc/user/project/img/markdown_logo.png diff --git a/doc/markdown/img/video.mp4 b/doc/user/project/img/markdown_video.mp4 Binary files differindex 1fc478842f5..1fc478842f5 100644 --- a/doc/markdown/img/video.mp4 +++ b/doc/user/project/img/markdown_video.mp4 diff --git a/doc/markdown/markdown.md b/doc/user/project/markdown.md index c6c7ac81c0d..7fe96e67dbb 100644 --- a/doc/markdown/markdown.md +++ b/doc/user/project/markdown.md @@ -28,6 +28,13 @@ * [Line Breaks](#line-breaks) * [Tables](#tables) +**[Wiki-Specific Markdown](#wiki-specific-markdown)** + +* [Wiki - Direct page link](#wiki-direct-page-link) +* [Wiki - Direct file link](#wiki-direct-file-link) +* [Wiki - Hierarchical link](#wiki-hierarchical-link) +* [Wiki - Root link](#wiki-root-link) + **[References](#references)** ## GitLab Flavored Markdown (GFM) @@ -331,11 +338,11 @@ The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`. Here's a sample video: - ![Sample Video](img/video.mp4) + ![Sample Video](img/markdown_video.mp4) Here's a sample video: -![Sample Video](img/video.mp4) +![Sample Video](img/markdown_video.mp4) # Standard Markdown @@ -533,24 +540,24 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown Here's our logo (hover to see the title text): Inline-style: - ![alt text](img/logo.png) + ![alt text](img/markdown_logo.png) Reference-style: ![alt text1][logo] - [logo]: img/logo.png + [logo]: img/markdown_logo.png Here's our logo: Inline-style: -![alt text](img/logo.png) +![alt text](img/markdown_logo.png) Reference-style: ![alt text][logo] -[logo]: img/logo.png +[logo]: img/markdown_logo.png ## Blockquotes @@ -692,6 +699,81 @@ By including colons in the header row, you can align the text within that column | Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | + +## Wiki-specific Markdown + +The following examples show how links inside wikis behave. + +### Wiki - Direct page link + +A link which just includes the slug for a page will point to that page, +_at the base level of the wiki_. + +This snippet would link to a `documentation` page at the root of your wiki: + +```markdown +[Link to Documentation](documentation) +``` + +### Wiki - Direct file link + +Links with a file extension point to that file, _relative to the current page_. + +If this snippet was placed on a page at `<your_wiki>/documentation/related`, +it would link to `<your_wiki>/documentation/file.md`: + +```markdown +[Link to File](file.md) +``` + +### Wiki - Hierarchical link + +A link can be constructed relative to the current wiki page using `./<page>`, +`../<page>`, etc. + +- If this snippet was placed on a page at `<your_wiki>/documentation/main`, + it would link to `<your_wiki>/documentation/related`: + + ```markdown + [Link to Related Page](./related) + ``` + +- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`, + it would link to `<your_wiki>/documentation/main`: + + ```markdown + [Link to Related Page](../main) + ``` + +- If this snippet was placed on a page at `<your_wiki>/documentation/main`, + it would link to `<your_wiki>/documentation/related.md`: + + ```markdown + [Link to Related Page](./related.md) + ``` + +- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`, + it would link to `<your_wiki>/documentation/main.md`: + + ```markdown + [Link to Related Page](../main.md) + ``` + +### Wiki - Root link + +A link starting with a `/` is relative to the wiki root. + +- This snippet links to `<wiki_root>/documentation`: + + ```markdown + [Link to Related Page](/documentation) + ``` + +- This snippet links to `<wiki_root>/miscellaneous.md`: + + ```markdown + [Link to Related Page](/miscellaneous.md) + ``` ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 0a42931147d..4bfb7e92e99 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -25,7 +25,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step 'project "Shop" has protected branches' do project = Project.find_by(name: "Shop") - project.protected_branches.create(name: "stable") + create(:protected_branch, project: project, name: "stable") end step 'I click new branch link' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 0fe046dcbf6..9a8896acb15 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -293,7 +293,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps first('.js-project-refs-dropdown').click page.within '.project-refs-form' do - click_link 'test' + click_link "'test'" end end diff --git a/lib/api/api.rb b/lib/api/api.rb index 3d7d67510a8..bd16806892b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -7,6 +7,10 @@ module API rack_response({ 'message' => '404 Not found' }.to_json, 404) end + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!({ messages: e.full_messages }, 400) + end + rescue_from :all do |exception| # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 # why is this not wrapped in something reusable? @@ -32,6 +36,7 @@ module API mount ::API::CommitStatuses mount ::API::Commits mount ::API::DeployKeys + mount ::API::Environments mount ::API::Files mount ::API::GroupMembers mount ::API::Groups diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 66b853eb342..a77afe634f6 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -35,6 +35,10 @@ module API # Protect a single branch # + # Note: The internal data model moved from `developers_can_{merge,push}` to `allowed_to_{merge,push}` + # in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility), + # but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`. + # # Parameters: # id (required) - The ID of a project # branch (required) - The name of the branch @@ -49,17 +53,36 @@ module API @branch = user_project.repository.find_branch(params[:branch]) not_found!('Branch') unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) - developers_can_push = to_boolean(params[:developers_can_push]) + developers_can_merge = to_boolean(params[:developers_can_merge]) + developers_can_push = to_boolean(params[:developers_can_push]) + + protected_branch_params = { + name: @branch.name + } + + unless developers_can_merge.nil? + protected_branch_params.merge!({ + merge_access_level_attributes: { + access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + } + }) + end + + unless developers_can_push.nil? + protected_branch_params.merge!({ + push_access_level_attributes: { + access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + } + }) + end if protected_branch - protected_branch.developers_can_push = developers_can_push unless developers_can_push.nil? - protected_branch.developers_can_merge = developers_can_merge unless developers_can_merge.nil? - protected_branch.save + service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params) + service.execute(protected_branch) else - user_project.protected_branches.create(name: @branch.name, - developers_can_push: developers_can_push || false, - developers_can_merge: developers_can_merge || false) + service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params) + service.execute end present @branch, with: Entities::RepoBranch, project: user_project diff --git a/lib/api/entities.rb b/lib/api/entities.rb index fbf0d74663f..3e21b7a0b8a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -126,11 +126,13 @@ module API end expose :developers_can_push do |repo_branch, options| - options[:project].developers_can_push_to_protected_branch? repo_branch.name + project = options[:project] + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER } end expose :developers_can_merge do |repo_branch, options| - options[:project].developers_can_merge_to_protected_branch? repo_branch.name + project = options[:project] + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER } end end @@ -149,8 +151,13 @@ module API expose :safe_message, as: :message end + class RepoCommitStats < Grape::Entity + expose :additions, :deletions, :total + end + class RepoCommitDetail < RepoCommit expose :parent_ids, :committed_date, :authored_date + expose :stats, using: Entities::RepoCommitStats expose :status end @@ -489,6 +496,10 @@ module API expose :key, :value end + class Environment < Grape::Entity + expose :id, :name, :external_url + end + class RepoLicense < Grape::Entity expose :key, :name, :nickname expose :featured, as: :popular diff --git a/lib/api/environments.rb b/lib/api/environments.rb new file mode 100644 index 00000000000..819f80d8365 --- /dev/null +++ b/lib/api/environments.rb @@ -0,0 +1,83 @@ +module API + # Environments RESTfull API endpoints + class Environments < Grape::API + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects do + desc 'Get all environments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + optional :page, type: Integer, desc: 'Page number of the current request' + optional :per_page, type: Integer, desc: 'Number of items per page' + end + get ':id/environments' do + authorize! :read_environment, user_project + + present paginate(user_project.environments), with: Entities::Environment + end + + desc 'Creates a new environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :name, type: String, desc: 'The name of the environment to be created' + optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + end + post ':id/environments' do + authorize! :create_environment, user_project + + create_params = declared(params, include_parent_namespaces: false).to_h + environment = user_project.environments.create(create_params) + + if environment.persisted? + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + desc 'Updates an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + optional :name, type: String, desc: 'The new environment name' + optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' + end + put ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h + if environment.update(update_params) + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + desc 'Deletes an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + delete ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + present environment.destroy, with: Entities::Environment + end + end + end +end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c588103e517..c4d3134da6c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -21,17 +21,6 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end - - def create_spam_log(project, current_user, attrs) - params = attrs.merge({ - source_ip: client_ip(env), - user_agent: user_agent(env), - noteable_type: 'Issue', - via_api: true - }) - - ::CreateSpamLogService.new(project, current_user, params).execute - end end resource :issues do @@ -168,15 +157,13 @@ module API end project = user_project - text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n") - if check_for_spam?(project, current_user) && is_spam?(env, current_user, text) - create_spam_log(project, current_user, attrs) + issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute + + if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end - issue = ::Issues::CreateService.new(project, current_user, attrs).execute - if issue.valid? # Find or create labels and attach to issue. Labels are valid because # we already checked its name, so there can't be an error here diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index ae7d31cf191..2492b5213ac 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -38,6 +38,11 @@ module Banzai end end + # Build a regexp that matches all valid :emoji: names. + def self.emoji_pattern + @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ + end + private def emoji_url(name) @@ -59,11 +64,6 @@ module Banzai ActionController::Base.helpers.url_to_image(image) end - # Build a regexp that matches all valid :emoji: names. - def self.emoji_pattern - @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ - end - def emoji_pattern self.class.emoji_pattern end diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index 9b209533a89..ff580ec68f8 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -12,7 +12,12 @@ module Banzai html end - private + def self.renderer + @renderer ||= begin + renderer = Redcarpet::Render::HTML.new + Redcarpet::Markdown.new(renderer, redcarpet_options) + end + end def self.redcarpet_options # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use @@ -28,12 +33,7 @@ module Banzai }.freeze end - def self.renderer - @renderer ||= begin - renderer = Redcarpet::Render::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) - end - end + private_class_method :redcarpet_options end end end diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 337fb50317d..5b73fc8fcee 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -87,10 +87,13 @@ module Banzai def build_relative_path(path, request_path) return request_path if path.empty? return path unless request_path + return path[1..-1] if path.start_with?('/') parts = request_path.split('/') parts.pop if uri_type(request_path) != :tree + path.sub!(%r{^\./}, '') + while path.start_with?('../') parts.pop path.sub!('../', '') diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index f306079d833..6c20dec5734 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -9,10 +9,11 @@ module Banzai issues = issues_for_nodes(nodes) - nodes.select do |node| - issue = issue_for_node(issues, node) + readable_issues = Ability. + issues_readable_by_user(issues.values, user).to_set - issue ? can?(user, :read_issue, issue) : false + nodes.select do |node| + readable_issues.include?(issue_for_node(issues, node)) end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 910687a7b6a..a4ae27eefd8 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,5 +1,7 @@ module Banzai module Renderer + extend self + # Convert a Markdown String into an HTML-safe String of HTML # # Note that while the returned HTML will have been sanitized of dangerous @@ -14,7 +16,7 @@ module Banzai # context - Hash of context options passed to our HTML Pipeline # # Returns an HTML-safe String - def self.render(text, context = {}) + def render(text, context = {}) cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) @@ -52,7 +54,7 @@ module Banzai # texts_and_contexts # => [{ text: '### Hello', # context: { cache_key: [note, :note] } }] - def self.cache_collection_render(texts_and_contexts) + def cache_collection_render(texts_and_contexts) items_collection = texts_and_contexts.each_with_index do |item, index| context = item[:context] cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline]) @@ -81,7 +83,7 @@ module Banzai items_collection.map { |item| item[:rendered] } end - def self.render_result(text, context = {}) + def render_result(text, context = {}) text = Pipeline[:pre_process].to_html(text, context) if text Pipeline[context[:pipeline]].call(text, context) @@ -100,7 +102,7 @@ module Banzai # :user - User object # # Returns an HTML-safe String - def self.post_process(html, context) + def post_process(html, context) context = Pipeline[context[:pipeline]].transform_context(context) pipeline = Pipeline[:post_process] @@ -113,7 +115,7 @@ module Banzai private - def self.cacheless_render(text, context = {}) + def cacheless_render(text, context = {}) Gitlab::Metrics.measure(:banzai_cacheless_render) do result = render_result(text, context) @@ -126,7 +128,7 @@ module Banzai end end - def self.full_cache_key(cache_key, pipeline_name) + def full_cache_key(cache_key, pipeline_name) return unless cache_key ["banzai", *cache_key, pipeline_name || :full] end @@ -134,7 +136,7 @@ module Banzai # To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key. # Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key # method. - def self.full_cache_multi_key(cache_key, pipeline_name) + def full_cache_multi_key(cache_key, pipeline_name) return unless cache_key Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) end diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 1d7126a432d..3decc3b1a26 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -1,5 +1,37 @@ module Ci module Charts + module DailyInterval + def grouped_count(query) + query. + group("DATE(#{Ci::Build.table_name}.created_at)"). + count(:created_at). + transform_keys { |date| date.strftime(@format) } + end + + def interval_step + @interval_step ||= 1.day + end + end + + module MonthlyInterval + def grouped_count(query) + if Gitlab::Database.postgresql? + query. + group("to_char(#{Ci::Build.table_name}.created_at, '01 Month YYYY')"). + count(:created_at). + transform_keys(&:squish) + else + query. + group("DATE_FORMAT(#{Ci::Build.table_name}.created_at, '01 %M %Y')"). + count(:created_at) + end + end + + def interval_step + @interval_step ||= 1.month + end + end + class Chart attr_reader :labels, :total, :success, :project, :build_times @@ -13,47 +45,59 @@ module Ci collect end - def push(from, to, format) - @labels << from.strftime(format) - @total << project.builds. - where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). - count(:all) - @success << project.builds. - where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). - success.count(:all) + def collect + query = project.builds. + where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", @to, @from) + + totals_count = grouped_count(query) + success_count = grouped_count(query.success) + + current = @from + while current < @to + label = current.strftime(@format) + + @labels << label + @total << (totals_count[label] || 0) + @success << (success_count[label] || 0) + + current += interval_step + end end end class YearChart < Chart - def collect - 13.times do |i| - start_month = (Date.today.years_ago(1) + i.month).beginning_of_month - end_month = start_month.end_of_month + include MonthlyInterval - push(start_month, end_month, "%d %B %Y") - end + def initialize(*) + @to = Date.today.end_of_month + @from = @to.years_ago(1).beginning_of_month + @format = '%d %B %Y' + + super end end class MonthChart < Chart - def collect - 30.times do |i| - start_day = Date.today - 30.days + i.days - end_day = Date.today - 30.days + i.day + 1.day + include DailyInterval - push(start_day, end_day, "%d %B") - end + def initialize(*) + @to = Date.today + @from = @to - 30.days + @format = '%d %B' + + super end end class WeekChart < Chart - def collect - 7.times do |i| - start_day = Date.today - 7.days + i.days - end_day = Date.today - 7.days + i.day + 1.day + include DailyInterval - push(start_day, end_day, "%d %B") - end + def initialize(*) + @to = Date.today + @from = @to - 7.days + @format = '%d %B' + + super end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 83afed9f49f..a2e8bd22a52 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -4,21 +4,11 @@ module Ci include Gitlab::Ci::Config::Node::LegacyValidationHelpers - DEFAULT_STAGE = 'test' - ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache] - ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, - :allow_failure, :type, :stage, :when, :artifacts, :cache, - :dependencies, :before_script, :after_script, :variables, - :environment] - ALLOWED_CACHE_KEYS = [:key, :untracked, :paths] - ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in] - attr_reader :path, :cache, :stages def initialize(config, path = nil) @ci_config = Gitlab::Ci::Config.new(config) @config = @ci_config.to_hash - @path = path unless @ci_config.valid? @@ -26,7 +16,6 @@ module Ci end initial_parsing - validate! rescue Gitlab::Ci::Config::Loader::FormatError => e raise ValidationError, e.message end @@ -73,7 +62,7 @@ module Ci # - before script should be a concatenated command commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), tag_list: job[:tags] || [], - name: name, + name: job[:name], allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment], @@ -92,6 +81,9 @@ module Ci private def initial_parsing + ## + # Global config + # @before_script = @ci_config.before_script @image = @ci_config.image @after_script = @ci_config.after_script @@ -100,34 +92,28 @@ module Ci @stages = @ci_config.stages @cache = @ci_config.cache - @jobs = {} - - @config.except!(*ALLOWED_YAML_KEYS) - @config.each { |name, param| add_job(name, param) } - - raise ValidationError, "Please define at least one job" if @jobs.none? - end - - def add_job(name, job) - return if name.to_s.start_with?('.') + ## + # Jobs + # + @jobs = @ci_config.jobs - raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) + @jobs.each do |name, job| + # logical validation for job - stage = job[:stage] || job[:type] || DEFAULT_STAGE - @jobs[name] = { stage: stage }.merge(job) + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) + end end def yaml_variables(name) - variables = global_variables.merge(job_variables(name)) + variables = (@variables || {}) + .merge(job_variables(name)) + variables.map do |key, value| { key: key, value: value, public: true } end end - def global_variables - @variables || {} - end - def job_variables(name) job = @jobs[name.to_sym] return {} unless job @@ -135,154 +121,16 @@ module Ci job[:variables] || {} end - def validate! - @jobs.each do |name, job| - validate_job!(name, job) - end - - true - end - - def validate_job!(name, job) - validate_job_name!(name) - validate_job_keys!(name, job) - validate_job_types!(name, job) - validate_job_script!(name, job) - - validate_job_stage!(name, job) if job[:stage] - validate_job_variables!(name, job) if job[:variables] - validate_job_cache!(name, job) if job[:cache] - validate_job_artifacts!(name, job) if job[:artifacts] - validate_job_dependencies!(name, job) if job[:dependencies] - end - - def validate_job_name!(name) - if name.blank? || !validate_string(name) - raise ValidationError, "job name should be non-empty string" - end - end - - def validate_job_keys!(name, job) - job.keys.each do |key| - unless ALLOWED_JOB_KEYS.include? key - raise ValidationError, "#{name} job: unknown parameter #{key}" - end - end - end - - def validate_job_types!(name, job) - if job[:image] && !validate_string(job[:image]) - raise ValidationError, "#{name} job: image should be a string" - end - - if job[:services] && !validate_array_of_strings(job[:services]) - raise ValidationError, "#{name} job: services should be an array of strings" - end - - if job[:tags] && !validate_array_of_strings(job[:tags]) - raise ValidationError, "#{name} job: tags parameter should be an array of strings" - end - - if job[:only] && !validate_array_of_strings_or_regexps(job[:only]) - raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps" - end - - if job[:except] && !validate_array_of_strings_or_regexps(job[:except]) - raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps" - end - - if job[:allow_failure] && !validate_boolean(job[:allow_failure]) - raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" - end - - if job[:when] && !job[:when].in?(%w[on_success on_failure always manual]) - raise ValidationError, "#{name} job: when parameter should be on_success, on_failure, always or manual" - end - - if job[:environment] && !validate_environment(job[:environment]) - raise ValidationError, "#{name} job: environment parameter #{Gitlab::Regex.environment_name_regex_message}" - end - end - - def validate_job_script!(name, job) - if !validate_string(job[:script]) && !validate_array_of_strings(job[:script]) - raise ValidationError, "#{name} job: script should be a string or an array of a strings" - end - - if job[:before_script] && !validate_array_of_strings(job[:before_script]) - raise ValidationError, "#{name} job: before_script should be an array of strings" - end - - if job[:after_script] && !validate_array_of_strings(job[:after_script]) - raise ValidationError, "#{name} job: after_script should be an array of strings" - end - end - def validate_job_stage!(name, job) + return unless job[:stage] + unless job[:stage].is_a?(String) && job[:stage].in?(@stages) raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" end end - def validate_job_variables!(name, job) - unless validate_variables(job[:variables]) - raise ValidationError, - "#{name} job: variables should be a map of key-value strings" - end - end - - def validate_job_cache!(name, job) - job[:cache].keys.each do |key| - unless ALLOWED_CACHE_KEYS.include? key - raise ValidationError, "#{name} job: cache unknown parameter #{key}" - end - end - - if job[:cache][:key] && !validate_string(job[:cache][:key]) - raise ValidationError, "#{name} job: cache:key parameter should be a string" - end - - if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked]) - raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean" - end - - if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths]) - raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings" - end - end - - def validate_job_artifacts!(name, job) - job[:artifacts].keys.each do |key| - unless ALLOWED_ARTIFACTS_KEYS.include? key - raise ValidationError, "#{name} job: artifacts unknown parameter #{key}" - end - end - - if job[:artifacts][:name] && !validate_string(job[:artifacts][:name]) - raise ValidationError, "#{name} job: artifacts:name parameter should be a string" - end - - if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked]) - raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean" - end - - if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths]) - raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings" - end - - if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always]) - raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always" - end - - if job[:artifacts][:expire_in] && !validate_duration(job[:artifacts][:expire_in]) - raise ValidationError, "#{name} job: artifacts:expire_in parameter should be a duration" - end - end - def validate_job_dependencies!(name, job) - unless validate_array_of_strings(job[:dependencies]) - raise ValidationError, "#{name} job: dependencies parameter should be an array of strings" - end + return unless job[:dependencies] stage_index = @stages.index(job[:stage]) diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb index 04676fdb748..207736b59db 100644 --- a/lib/gitlab/akismet_helper.rb +++ b/lib/gitlab/akismet_helper.rb @@ -17,8 +17,8 @@ module Gitlab env['HTTP_USER_AGENT'] end - def check_for_spam?(project, user) - akismet_enabled? && !project.team.member?(user) + def check_for_spam?(project) + akismet_enabled? && project.public? end def is_spam?(environment, user, text) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index e6cc1529760..ae82c0db3f1 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -8,7 +8,7 @@ module Gitlab # Temporary delegations that should be removed after refactoring # delegate :before_script, :image, :services, :after_script, :variables, - :stages, :cache, to: :@global + :stages, :cache, :jobs, to: :@global def initialize(config) @config = Loader.new(config).load! diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/node/artifacts.rb new file mode 100644 index 00000000000..844bd2fe998 --- /dev/null +++ b/lib/gitlab/ci/config/node/artifacts.rb @@ -0,0 +1,35 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a configuration of job artifacts. + # + class Artifacts < Entry + include Validatable + include Attributable + + ALLOWED_KEYS = %i[name untracked paths when expire_in] + + attributes ALLOWED_KEYS + + validations do + validates :config, type: Hash + validates :config, allowed_keys: ALLOWED_KEYS + + with_options allow_nil: true do + validates :name, type: String + validates :untracked, boolean: true + validates :paths, array_of_strings: true + validates :when, + inclusion: { in: %w[on_success on_failure always], + message: 'should be on_success, on_failure ' \ + 'or always' } + validates :expire_in, duration: true + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/attributable.rb b/lib/gitlab/ci/config/node/attributable.rb new file mode 100644 index 00000000000..221b666f9f6 --- /dev/null +++ b/lib/gitlab/ci/config/node/attributable.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + class Config + module Node + module Attributable + extend ActiveSupport::Concern + + class_methods do + def attributes(*attributes) + attributes.flatten.each do |attribute| + define_method(attribute) do + return unless config.is_a?(Hash) + + config[attribute] + end + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/cache.rb b/lib/gitlab/ci/config/node/cache.rb index cdf8ba2e35d..b4bda2841ac 100644 --- a/lib/gitlab/ci/config/node/cache.rb +++ b/lib/gitlab/ci/config/node/cache.rb @@ -8,6 +8,12 @@ module Gitlab class Cache < Entry include Configurable + ALLOWED_KEYS = %i[key untracked paths] + + validations do + validates :config, allowed_keys: ALLOWED_KEYS + end + node :key, Node::Key, description: 'Cache key used to define a cache affinity.' @@ -16,10 +22,6 @@ module Gitlab node :paths, Node::Paths, description: 'Specify which paths should be cached across builds.' - - validations do - validates :config, allowed_keys: true - end end end end diff --git a/lib/gitlab/ci/config/node/commands.rb b/lib/gitlab/ci/config/node/commands.rb new file mode 100644 index 00000000000..d7657ae314b --- /dev/null +++ b/lib/gitlab/ci/config/node/commands.rb @@ -0,0 +1,33 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a job script. + # + class Commands < Entry + include Validatable + + validations do + include LegacyValidationHelpers + + validate do + unless string_or_array_of_strings?(config) + errors.add(:config, + 'should be a string or an array of strings') + end + end + + def string_or_array_of_strings?(field) + validate_string(field) || validate_array_of_strings(field) + end + end + + def value + Array(@config) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 37936fc8242..2de82d40c9d 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -25,10 +25,14 @@ module Gitlab private - def create_node(key, factory) - factory.with(value: @config[key], key: key, parent: self) + def compose! + self.class.nodes.each do |key, factory| + factory + .value(@config[key]) + .with(key: key, parent: self) - factory.create! + @entries[key] = factory.create! + end end class_methods do @@ -36,24 +40,25 @@ module Gitlab Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] end - private + private # rubocop:disable Lint/UselessAccessModifier - def node(symbol, entry_class, metadata) - factory = Node::Factory.new(entry_class) + def node(key, node, metadata) + factory = Node::Factory.new(node) .with(description: metadata[:description]) - (@nodes ||= {}).merge!(symbol.to_sym => factory) + (@nodes ||= {}).merge!(key.to_sym => factory) end def helpers(*nodes) nodes.each do |symbol| define_method("#{symbol}_defined?") do - @nodes[symbol].try(:defined?) + @entries[symbol].specified? if @entries[symbol] end define_method("#{symbol}_value") do - raise Entry::InvalidError unless valid? - @nodes[symbol].try(:value) + return unless @entries[symbol] && @entries[symbol].valid? + + @entries[symbol].value end alias_method symbol.to_sym, "#{symbol}_value".to_sym diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 9e79e170a4f..0c782c422b5 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -8,30 +8,31 @@ module Gitlab class Entry class InvalidError < StandardError; end - attr_reader :config + attr_reader :config, :metadata attr_accessor :key, :parent, :description - def initialize(config) + def initialize(config, **metadata) @config = config - @nodes = {} + @metadata = metadata + @entries = {} + @validator = self.class.validator.new(self) - @validator.validate + @validator.validate(:new) end def process! - return if leaf? return unless valid? compose! - process_nodes! + descendants.each(&:process!) end - def nodes - @nodes.values + def leaf? + @entries.none? end - def leaf? - self.class.nodes.none? + def descendants + @entries.values end def ancestors @@ -43,27 +44,30 @@ module Gitlab end def errors - @validator.messages + nodes.flat_map(&:errors) + @validator.messages + descendants.flat_map(&:errors) end def value if leaf? @config else - defined = @nodes.select { |_key, value| value.defined? } - Hash[defined.map { |key, node| [key, node.value] }] + meaningful = @entries.select do |_key, value| + value.specified? && value.relevant? + end + + Hash[meaningful.map { |key, entry| [key, entry.value] }] end end - def defined? + def specified? true end - def self.default + def relevant? + true end - def self.nodes - {} + def self.default end def self.validator @@ -73,17 +77,6 @@ module Gitlab private def compose! - self.class.nodes.each do |key, essence| - @nodes[key] = create_node(key, essence) - end - end - - def process_nodes! - nodes.each(&:process!) - end - - def create_node(key, essence) - raise NotImplementedError end end end diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 5919a283283..707b052e6a8 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -10,35 +10,60 @@ module Gitlab def initialize(node) @node = node + @metadata = {} @attributes = {} end + def value(value) + @value = value + self + end + + def metadata(metadata) + @metadata.merge!(metadata) + self + end + def with(attributes) @attributes.merge!(attributes) self end def create! - raise InvalidFactory unless @attributes.has_key?(:value) + raise InvalidFactory unless defined?(@value) - fabricate.tap do |entry| - entry.key = @attributes[:key] - entry.parent = @attributes[:parent] - entry.description = @attributes[:description] + ## + # We assume that unspecified entry is undefined. + # See issue #18775. + # + if @value.nil? + Node::Undefined.new( + fabricate_undefined + ) + else + fabricate(@node, @value) end end private - def fabricate + def fabricate_undefined ## - # We assume that unspecified entry is undefined. - # See issue #18775. + # If node has a default value we fabricate concrete node + # with default value. # - if @attributes[:value].nil? - Node::Undefined.new(@node) + if @node.default.nil? + fabricate(Node::Null) else - @node.new(@attributes[:value]) + fabricate(@node, @node.default) + end + end + + def fabricate(node, value = nil) + node.new(value, @metadata).tap do |entry| + entry.key = @attributes[:key] + entry.parent = @attributes[:parent] + entry.description = @attributes[:description] end end end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index f92e1eccbcf..ccd539fb003 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -34,10 +34,36 @@ module Gitlab description: 'Configure caching between build jobs.' helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache + :variables, :stages, :types, :cache, :jobs - def stages - stages_defined? ? stages_value : types_value + private + + def compose! + super + + compose_jobs! + compose_deprecated_entries! + end + + def compose_jobs! + factory = Node::Factory.new(Node::Jobs) + .value(@config.except(*self.class.nodes.keys)) + .with(key: :jobs, parent: self, + description: 'Jobs definition for this pipeline') + + @entries[:jobs] = factory.create! + end + + def compose_deprecated_entries! + ## + # Deprecated `:types` key workaround - if types are defined and + # stages are not defined we use types definition as stages. + # + if types_defined? && !stages_defined? + @entries[:stages] = @entries[:types] + end + + @entries.delete(:types) end end end diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb new file mode 100644 index 00000000000..073044b66f8 --- /dev/null +++ b/lib/gitlab/ci/config/node/hidden_job.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a hidden CI/CD job. + # + class HiddenJob < Entry + include Validatable + + validations do + validates :config, type: Hash + validates :config, presence: true + end + + def relevant? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb new file mode 100644 index 00000000000..e84737acbb9 --- /dev/null +++ b/lib/gitlab/ci/config/node/job.rb @@ -0,0 +1,123 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a concrete CI/CD job. + # + class Job < Entry + include Configurable + include Attributable + + ALLOWED_KEYS = %i[tags script only except type image services allow_failure + type stage when artifacts cache dependencies before_script + after_script variables environment] + + attributes :tags, :allow_failure, :when, :environment, :dependencies + + validations do + validates :config, allowed_keys: ALLOWED_KEYS + + validates :config, presence: true + validates :name, presence: true + validates :name, type: Symbol + + with_options allow_nil: true do + validates :tags, array_of_strings: true + validates :allow_failure, boolean: true + validates :when, + inclusion: { in: %w[on_success on_failure always manual], + message: 'should be on_success, on_failure, ' \ + 'always or manual' } + validates :environment, + type: { + with: String, + message: Gitlab::Regex.environment_name_regex_message } + validates :environment, + format: { + with: Gitlab::Regex.environment_name_regex, + message: Gitlab::Regex.environment_name_regex_message } + + validates :dependencies, array_of_strings: true + end + end + + node :before_script, Script, + description: 'Global before script overridden in this job.' + + node :script, Commands, + description: 'Commands that will be executed in this job.' + + node :stage, Stage, + description: 'Pipeline stage this job will be executed into.' + + node :type, Stage, + description: 'Deprecated: stage this job will be executed into.' + + node :after_script, Script, + description: 'Commands that will be executed when finishing job.' + + node :cache, Cache, + description: 'Cache definition for this job.' + + node :image, Image, + description: 'Image that will be used to execute this job.' + + node :services, Services, + description: 'Services that will be used to execute this job.' + + node :only, Trigger, + description: 'Refs policy this job will be executed for.' + + node :except, Trigger, + description: 'Refs policy this job will be executed for.' + + node :variables, Variables, + description: 'Environment variables available for this job.' + + node :artifacts, Artifacts, + description: 'Artifacts configuration for this job.' + + helpers :before_script, :script, :stage, :type, :after_script, + :cache, :image, :services, :only, :except, :variables, + :artifacts + + def name + @metadata[:name] + end + + def value + @config.merge(to_hash.compact) + end + + private + + def to_hash + { name: name, + before_script: before_script, + script: script, + image: image, + services: services, + stage: stage, + cache: cache, + only: only, + except: except, + variables: variables_defined? ? variables : nil, + artifacts: artifacts, + after_script: after_script } + end + + def compose! + super + + if type_defined? && !stage_defined? + @entries[:stage] = @entries[:type] + end + + @entries.delete(:type) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb new file mode 100644 index 00000000000..51683c82ceb --- /dev/null +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -0,0 +1,48 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a set of jobs. + # + class Jobs < Entry + include Validatable + + validations do + validates :config, type: Hash + + validate do + unless has_visible_job? + errors.add(:config, 'should contain at least one visible job') + end + end + + def has_visible_job? + config.any? { |name, _| !hidden?(name) } + end + end + + def hidden?(name) + name.to_s.start_with?('.') + end + + private + + def compose! + @config.each do |name, config| + node = hidden?(name) ? Node::HiddenJob : Node::Job + + factory = Node::Factory.new(node) + .value(config || {}) + .metadata(name: name) + .with(key: name, parent: self, + description: "#{name} job definition.") + + @entries[name] = factory.create! + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb index 4d9a508796a..0c291efe6a5 100644 --- a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb @@ -41,10 +41,6 @@ module Gitlab false end - def validate_environment(value) - value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex - end - def validate_boolean(value) value.in?([true, false]) end diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb new file mode 100644 index 00000000000..88a5f53f13c --- /dev/null +++ b/lib/gitlab/ci/config/node/null.rb @@ -0,0 +1,34 @@ +module Gitlab + module Ci + class Config + module Node + ## + # This class represents an undefined node. + # + # Implements the Null Object pattern. + # + class Null < Entry + def value + nil + end + + def valid? + true + end + + def errors + [] + end + + def specified? + false + end + + def relevant? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb new file mode 100644 index 00000000000..cbc97641f5a --- /dev/null +++ b/lib/gitlab/ci/config/node/stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a stage for a job. + # + class Stage < Entry + include Validatable + + validations do + validates :config, type: String + end + + def self.default + 'test' + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/trigger.rb b/lib/gitlab/ci/config/node/trigger.rb new file mode 100644 index 00000000000..d8b31975088 --- /dev/null +++ b/lib/gitlab/ci/config/node/trigger.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a trigger policy for the job. + # + class Trigger < Entry + include Validatable + + validations do + include LegacyValidationHelpers + + validate :array_of_strings_or_regexps + + def array_of_strings_or_regexps + unless validate_array_of_strings_or_regexps(config) + errors.add(:config, 'should be an array of strings or regexps') + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index 699605e1e3a..45fef8c3ae5 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -3,24 +3,13 @@ module Gitlab class Config module Node ## - # This class represents an undefined entry node. + # This class represents an unspecified entry node. # - # It takes original entry class as configuration and returns default - # value of original entry as self value. + # It decorates original entry adding method that indicates it is + # unspecified. # - # - class Undefined < Entry - include Validatable - - validations do - validates :config, type: Class - end - - def value - @config.default - end - - def defined? + class Undefined < SimpleDelegator + def specified? false end end diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb index 758a6cf4356..43c7e102b50 100644 --- a/lib/gitlab/ci/config/node/validator.rb +++ b/lib/gitlab/ci/config/node/validator.rb @@ -21,18 +21,19 @@ module Gitlab 'Validator' end - def unknown_keys - return [] unless config.is_a?(Hash) - - config.keys - @node.class.nodes.keys - end - private def location predecessors = ancestors.map(&:key).compact - current = key || @node.class.name.demodulize.underscore - predecessors.append(current).join(':') + predecessors.append(key_name).join(':') + end + + def key_name + if key.blank? + @node.class.name.demodulize.underscore.humanize + else + key + end end end end diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index 7b2f57990b5..e20908ad3cb 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -5,10 +5,11 @@ module Gitlab module Validators class AllowedKeysValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if record.unknown_keys.any? - unknown_list = record.unknown_keys.join(', ') - record.errors.add(:config, - "contains unknown keys: #{unknown_list}") + unknown_keys = record.config.try(:keys).to_a - options[:in] + + if unknown_keys.any? + record.errors.add(:config, 'contains unknown keys: ' + + unknown_keys.join(', ')) end end end @@ -33,6 +34,16 @@ module Gitlab end end + class DurationValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_duration(value) + record.errors.add(attribute, 'should be a duration') + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers @@ -49,7 +60,8 @@ module Gitlab raise unless type.is_a?(Class) unless value.is_a?(type) - record.errors.add(attribute, "should be a #{type.name}") + message = options[:message] || "should be a #{type.name}" + record.errors.add(attribute, message) end end end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 078609c86f1..55b8f888d53 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -55,12 +55,12 @@ module Gitlab end end - private - def self.connection ActiveRecord::Base.connection end + private_class_method :connection + def self.database_version row = connection.execute("SELECT VERSION()").first @@ -70,5 +70,7 @@ module Gitlab row.first end end + + private_class_method :database_version end end diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 28ad637fda4..55708d42161 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -19,24 +19,6 @@ module Gitlab attr_accessor :old_line, :new_line, :offset - def self.for_lines(lines) - changed_line_pairs = self.find_changed_line_pairs(lines) - - inline_diffs = [] - - changed_line_pairs.each do |old_index, new_index| - old_line = lines[old_index] - new_line = lines[new_index] - - old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - - inline_diffs[old_index] = old_diffs - inline_diffs[new_index] = new_diffs - end - - inline_diffs - end - def initialize(old_line, new_line, offset: 0) @old_line = old_line[offset..-1] @new_line = new_line[offset..-1] @@ -63,32 +45,54 @@ module Gitlab [old_diffs, new_diffs] end - private + class << self + def for_lines(lines) + changed_line_pairs = find_changed_line_pairs(lines) - # Finds pairs of old/new line pairs that represent the same line that changed - def self.find_changed_line_pairs(lines) - # Prefixes of all diff lines, indicating their types - # For example: `" - + -+ ---+++ --+ -++"` - line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ') + inline_diffs = [] - changed_line_pairs = [] - line_prefixes.scan(LINE_PAIRS_PATTERN) do - # For `"---+++"`, `begin_index == 0`, `end_index == 6` - begin_index, end_index = Regexp.last_match.offset(:del_ins) + changed_line_pairs.each do |old_index, new_index| + old_line = lines[old_index] + new_line = lines[new_index] - # For `"---+++"`, `changed_line_count == 3` - changed_line_count = (end_index - begin_index) / 2 + old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - halfway_index = begin_index + changed_line_count - (begin_index...halfway_index).each do |i| - # For `"---+++"`, index 1 maps to 1 + 3 = 4 - changed_line_pairs << [i, i + changed_line_count] + inline_diffs[old_index] = old_diffs + inline_diffs[new_index] = new_diffs end + + inline_diffs end - changed_line_pairs + private + + # Finds pairs of old/new line pairs that represent the same line that changed + def find_changed_line_pairs(lines) + # Prefixes of all diff lines, indicating their types + # For example: `" - + -+ ---+++ --+ -++"` + line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ') + + changed_line_pairs = [] + line_prefixes.scan(LINE_PAIRS_PATTERN) do + # For `"---+++"`, `begin_index == 0`, `end_index == 6` + begin_index, end_index = Regexp.last_match.offset(:del_ins) + + # For `"---+++"`, `changed_line_count == 3` + changed_line_count = (end_index - begin_index) / 2 + + halfway_index = begin_index + changed_line_count + (begin_index...halfway_index).each do |i| + # For `"---+++"`, index 1 maps to 1 + 3 = 4 + changed_line_pairs << [i, i + changed_line_count] + end + end + + changed_line_pairs + end end + private + def longest_common_prefix(a, b) max_length = [a.length, b.length].max diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb new file mode 100644 index 00000000000..bd3267e2a80 --- /dev/null +++ b/lib/gitlab/email/handler.rb @@ -0,0 +1,17 @@ +require 'gitlab/email/handler/create_note_handler' +require 'gitlab/email/handler/create_issue_handler' + +module Gitlab + module Email + module Handler + HANDLERS = [CreateNoteHandler, CreateIssueHandler] + + def self.for(mail, mail_key) + HANDLERS.find do |klass| + handler = klass.new(mail, mail_key) + break handler if handler.can_handle? + end + end + end + end +end diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb new file mode 100644 index 00000000000..b7ed11cb638 --- /dev/null +++ b/lib/gitlab/email/handler/base_handler.rb @@ -0,0 +1,60 @@ +module Gitlab + module Email + module Handler + class BaseHandler + attr_reader :mail, :mail_key + + def initialize(mail, mail_key) + @mail = mail + @mail_key = mail_key + end + + def message + @message ||= process_message + end + + def author + raise NotImplementedError + end + + def project + raise NotImplementedError + end + + private + + def validate_permission!(permission) + raise UserNotFoundError unless author + raise UserBlockedError if author.blocked? + raise ProjectNotFound unless author.can?(:read_project, project) + raise UserNotAuthorizedError unless author.can?(permission, project) + end + + def process_message + message = ReplyParser.new(mail).execute.strip + add_attachments(message) + end + + def add_attachments(reply) + attachments = Email::AttachmentUploader.new(mail).execute(project) + + reply + attachments.map do |link| + "\n\n#{link[:markdown]}" + end.join + end + + def verify_record!(record:, invalid_exception:, record_name:) + return if record.persisted? + + error_title = "The #{record_name} could not be created for the following reasons:" + + msg = error_title + record.errors.full_messages.map do |error| + "\n\n- #{error}" + end.join + + raise invalid_exception, msg + end + end + end + end +end diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb new file mode 100644 index 00000000000..4e6566af8ab --- /dev/null +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -0,0 +1,52 @@ + +require 'gitlab/email/handler/base_handler' + +module Gitlab + module Email + module Handler + class CreateIssueHandler < BaseHandler + attr_reader :project_path, :authentication_token + + def initialize(mail, mail_key) + super(mail, mail_key) + @project_path, @authentication_token = + mail_key && mail_key.split('+', 2) + end + + def can_handle? + !authentication_token.nil? + end + + def execute + raise ProjectNotFound unless project + + validate_permission!(:create_issue) + + verify_record!( + record: create_issue, + invalid_exception: InvalidIssueError, + record_name: 'issue') + end + + def author + @author ||= User.find_by(authentication_token: authentication_token) + end + + def project + @project ||= Project.find_with_namespace(project_path) + end + + private + + def create_issue + Issues::CreateService.new( + project, + author, + title: mail.subject, + description: message + ).execute + end + end + end + end +end diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb new file mode 100644 index 00000000000..06dae31cc27 --- /dev/null +++ b/lib/gitlab/email/handler/create_note_handler.rb @@ -0,0 +1,55 @@ + +require 'gitlab/email/handler/base_handler' + +module Gitlab + module Email + module Handler + class CreateNoteHandler < BaseHandler + def can_handle? + mail_key =~ /\A\w+\z/ + end + + def execute + raise SentNotificationNotFoundError unless sent_notification + raise AutoGeneratedEmailError if mail.header.to_s =~ /auto-(generated|replied)/ + + validate_permission!(:create_note) + + raise NoteableNotFoundError unless sent_notification.noteable + raise EmptyEmailError if message.blank? + + verify_record!( + record: create_note, + invalid_exception: InvalidNoteError, + record_name: 'comment') + end + + def author + sent_notification.recipient + end + + def project + sent_notification.project + end + + def sent_notification + @sent_notification ||= SentNotification.for(mail_key) + end + + private + + def create_note + Notes::CreateService.new( + project, + author, + note: message, + noteable_type: sent_notification.noteable_type, + noteable_id: sent_notification.noteable_id, + commit_id: sent_notification.commit_id, + line_code: sent_notification.line_code + ).execute + end + end + end + end +end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 1c671a7487b..9213cfb51e8 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -1,18 +1,24 @@ + +require 'gitlab/email/handler' + # Inspired in great part by Discourse's Email::Receiver module Gitlab module Email - class Receiver - class ProcessingError < StandardError; end - class EmailUnparsableError < ProcessingError; end - class SentNotificationNotFoundError < ProcessingError; end - class EmptyEmailError < ProcessingError; end - class AutoGeneratedEmailError < ProcessingError; end - class UserNotFoundError < ProcessingError; end - class UserBlockedError < ProcessingError; end - class UserNotAuthorizedError < ProcessingError; end - class NoteableNotFoundError < ProcessingError; end - class InvalidNoteError < ProcessingError; end + class ProcessingError < StandardError; end + class EmailUnparsableError < ProcessingError; end + class SentNotificationNotFoundError < ProcessingError; end + class ProjectNotFound < ProcessingError; end + class EmptyEmailError < ProcessingError; end + class AutoGeneratedEmailError < ProcessingError; end + class UserNotFoundError < ProcessingError; end + class UserBlockedError < ProcessingError; end + class UserNotAuthorizedError < ProcessingError; end + class NoteableNotFoundError < ProcessingError; end + class InvalidNoteError < ProcessingError; end + class InvalidIssueError < ProcessingError; end + class UnknownIncomingEmail < ProcessingError; end + class Receiver def initialize(raw) @raw = raw end @@ -20,91 +26,38 @@ module Gitlab def execute raise EmptyEmailError if @raw.blank? - raise SentNotificationNotFoundError unless sent_notification - - raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/ - - author = sent_notification.recipient - - raise UserNotFoundError unless author - - raise UserBlockedError if author.blocked? - - project = sent_notification.project - - raise UserNotAuthorizedError unless project && author.can?(:create_note, project) - - raise NoteableNotFoundError unless sent_notification.noteable - - reply = ReplyParser.new(message).execute.strip - - raise EmptyEmailError if reply.blank? - - reply = add_attachments(reply) - - note = create_note(reply) + mail = build_mail + mail_key = extract_mail_key(mail) + handler = Handler.for(mail, mail_key) - unless note.persisted? - msg = "The comment could not be created for the following reasons:" - note.errors.full_messages.each do |error| - msg << "\n\n- #{error}" - end + raise UnknownIncomingEmail unless handler - raise InvalidNoteError, msg - end + handler.execute end - private - - def message - @message ||= Mail::Message.new(@raw) - rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e + def build_mail + Mail::Message.new(@raw) + rescue Encoding::UndefinedConversionError, + Encoding::InvalidByteSequenceError => e raise EmailUnparsableError, e end - def reply_key - key_from_to_header || key_from_additional_headers + def extract_mail_key(mail) + key_from_to_header(mail) || key_from_additional_headers(mail) end - def key_from_to_header - key = nil - message.to.each do |address| + def key_from_to_header(mail) + mail.to.find do |address| key = Gitlab::IncomingEmail.key_from_address(address) - break if key + break key if key end - - key end - def key_from_additional_headers - reply_key = nil - - Array(message.references).each do |message_id| - reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id) - break if reply_key + def key_from_additional_headers(mail) + Array(mail.references).find do |mail_id| + key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id) + break key if key end - - reply_key - end - - def sent_notification - return nil unless reply_key - - SentNotification.for(reply_key) - end - - def add_attachments(reply) - attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project) - - attachments.each do |link| - reply << "\n\n#{link[:markdown]}" - end - - reply - end - - def create_note(reply) - sent_notification.create_note(reply) end end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index d6d14bd98a0..48b2c43ac21 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -2,7 +2,7 @@ module Gitlab module ImportExport extend self - VERSION = '0.1.2' + VERSION = '0.1.3' FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 5dd0e34c18e..e522a0fc8f6 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -17,6 +17,10 @@ module Gitlab execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) end + def git_restore_hooks + execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args) + end + private def tar_with_options(archive:, dir:, options:) @@ -45,6 +49,10 @@ module Gitlab FileUtils.copy_entry(source, destination) true end + + def repository_storage_paths_args + Gitlab.config.repositories.storages.values + end end end end diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 82d1e1805c5..eca6e5b6d51 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -3,6 +3,8 @@ module Gitlab class FileImporter include Gitlab::ImportExport::CommandLineUtil + MAX_RETRIES = 8 + def self.import(*args) new(*args).import end @@ -14,7 +16,10 @@ module Gitlab def import FileUtils.mkdir_p(@shared.export_path) - decompress_archive + + wait_for_archived_file do + decompress_archive + end rescue => e @shared.error(e) false @@ -22,6 +27,17 @@ module Gitlab private + # Exponentially sleep until I/O finishes copying the file + def wait_for_archived_file + MAX_RETRIES.times do |retry_number| + break if File.exist?(@archive_file) + + sleep(2**retry_number) + end + + yield + end + def decompress_archive result = untar_zxf(archive: @archive_file, dir: @shared.export_path) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 15afe8174a4..1da51043611 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -3,11 +3,12 @@ project_tree: - issues: - :events - notes: - - :author - - :events - - :labels - - milestones: - - :events + - :author + - :events + - label_links: + - :label + - milestone: + - :events - snippets: - notes: :author @@ -20,6 +21,10 @@ project_tree: - :events - :merge_request_diff - :events + - label_links: + - :label + - milestone: + - :events - pipelines: - notes: - :author @@ -31,6 +36,9 @@ project_tree: - :services - :hooks - :protected_branches + - :labels + - milestones: + - :events # Only include the following attributes for the models specified. included_attributes: @@ -55,6 +63,10 @@ excluded_attributes: - :expired_at merge_request_diff: - :st_diffs + issues: + - :milestone_id + merge_requests: + - :milestone_id methods: statuses: diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb new file mode 100644 index 00000000000..008300bde45 --- /dev/null +++ b/lib/gitlab/import_export/json_hash_builder.rb @@ -0,0 +1,110 @@ +module Gitlab + module ImportExport + # Generates a hash that conforms with http://apidock.com/rails/Hash/to_json + # and its peculiar options. + class JsonHashBuilder + def self.build(model_objects, attributes_finder) + new(model_objects, attributes_finder).build + end + + def initialize(model_objects, attributes_finder) + @model_objects = model_objects + @attributes_finder = attributes_finder + end + + def build + process_model_objects(@model_objects) + end + + private + + # Called when the model is actually a hash containing other relations (more models) + # Returns the config in the right format for calling +to_json+ + # + # +model_object_hash+ - A model relationship such as: + # {:merge_requests=>[:merge_request_diff, :notes]} + def process_model_objects(model_object_hash) + json_config_hash = {} + current_key = model_object_hash.keys.first + + model_object_hash.values.flatten.each do |model_object| + @attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash } + handle_model_object(current_key, model_object, json_config_hash) + end + + json_config_hash + end + + # Creates or adds to an existing hash an individual model or list + # + # +current_key+ main model that will be a key in the hash + # +model_object+ model or list of models to include in the hash + # +json_config_hash+ the original hash containing the root model + def handle_model_object(current_key, model_object, json_config_hash) + model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object + + if json_config_hash[current_key] + add_model_value(current_key, model_or_sub_model, json_config_hash) + else + create_model_value(current_key, model_or_sub_model, json_config_hash) + end + end + + # Constructs a new hash that will hold the configuration for that particular object + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + # +json_config_hash+ the original hash containing the root model + def create_model_value(current_key, value, json_config_hash) + parsed_hash = { include: value } + parse_hash(value, parsed_hash) + + json_config_hash[current_key] = parsed_hash + end + + # Calls attributes finder to parse the hash and add any attributes to it + # + # +value+ existing model to be included in the hash + # +parsed_hash+ the original hash + def parse_hash(value, parsed_hash) + @attributes_finder.parse(value) do |hash| + parsed_hash = { include: hash_or_merge(value, hash) } + end + end + + # Adds new model configuration to an existing hash with key +current_key+ + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + # +json_config_hash+ the original hash containing the root model + def add_model_value(current_key, value, json_config_hash) + @attributes_finder.parse(value) { |hash| value = { value => hash } } + + add_to_array(current_key, json_config_hash, value) + end + + # Adds new model configuration to an existing hash with key +current_key+ + # it creates a new array if it was previously a single value + # + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + # +json_config_hash+ the original hash containing the root model + def add_to_array(current_key, json_config_hash, value) + old_values = json_config_hash[current_key][:include] + + json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten + end + + # Construct a new hash or merge with an existing one a model configuration + # This is to fulfil +to_json+ requirements. + # + # +hash+ hash containing configuration generated mainly from +@attributes_finder+ + # +value+ existing model to be included in the hash + def hash_or_merge(value, hash) + value.is_a?(Hash) ? value.merge(hash) : { value => hash } + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 051110c23cf..c7b3551b84c 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -47,7 +47,7 @@ module Gitlab relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) - saved << restored_project.update_attribute(relation_key, relation_hash) + saved << restored_project.append_or_update_attribute(relation_key, relation_hash) end saved.all? end @@ -78,7 +78,7 @@ module Gitlab relation_key = relation.keys.first.to_s return if tree_hash[relation_key].blank? - tree_hash[relation_key].each do |relation_item| + [tree_hash[relation_key]].flatten.each do |relation_item| relation.values.flatten.each do |sub_relation| # We just use author to get the user ID, do not attempt to create an instance. next if sub_relation == :author diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb index 15f5dd31035..5021a1a14ce 100644 --- a/lib/gitlab/import_export/reader.rb +++ b/lib/gitlab/import_export/reader.rb @@ -29,87 +29,12 @@ module Gitlab def build_hash(model_list) model_list.map do |model_objects| if model_objects.is_a?(Hash) - build_json_config_hash(model_objects) + Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder) else @attributes_finder.find(model_objects) end end end - - # Called when the model is actually a hash containing other relations (more models) - # Returns the config in the right format for calling +to_json+ - # +model_object_hash+ - A model relationship such as: - # {:merge_requests=>[:merge_request_diff, :notes]} - def build_json_config_hash(model_object_hash) - @json_config_hash = {} - - model_object_hash.values.flatten.each do |model_object| - current_key = model_object_hash.keys.first - - @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } - - handle_model_object(current_key, model_object) - process_sub_model(current_key, model_object) if model_object.is_a?(Hash) - end - @json_config_hash - end - - # If the model is a hash, process the sub_models, which could also be hashes - # If there is a list, add to an existing array, otherwise use hash syntax - # +current_key+ main model that will be a key in the hash - # +model_object+ model or list of models to include in the hash - def process_sub_model(current_key, model_object) - sub_model_json = build_json_config_hash(model_object).dup - @json_config_hash.slice!(current_key) - - if @json_config_hash[current_key] && @json_config_hash[current_key][:include] - @json_config_hash[current_key][:include] << sub_model_json - else - @json_config_hash[current_key] = { include: sub_model_json } - end - end - - # Creates or adds to an existing hash an individual model or list - # +current_key+ main model that will be a key in the hash - # +model_object+ model or list of models to include in the hash - def handle_model_object(current_key, model_object) - if @json_config_hash[current_key] - add_model_value(current_key, model_object) - else - create_model_value(current_key, model_object) - end - end - - # Constructs a new hash that will hold the configuration for that particular object - # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - def create_model_value(current_key, value) - parsed_hash = { include: value } - - @attributes_finder.parse(value) do |hash| - parsed_hash = { include: hash_or_merge(value, hash) } - end - @json_config_hash[current_key] = parsed_hash - end - - # Adds new model configuration to an existing hash with key +current_key+ - # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - def add_model_value(current_key, value) - @attributes_finder.parse(value) { |hash| value = { value => hash } } - old_values = @json_config_hash[current_key][:include] - @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten - end - - # Construct a new hash or merge with an existing one a model configuration - # This is to fulfil +to_json+ requirements. - # +value+ existing model to be included in the hash - # +hash+ hash containing configuration generated mainly from +@attributes_finder+ - def hash_or_merge(value, hash) - value.is_a?(Hash) ? value.merge(hash) : { value => hash } - end end end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index e41c7e6bf4f..5e56b3d1aa7 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -13,6 +13,10 @@ module Gitlab BUILD_MODELS = %w[Ci::Build commit_status].freeze + IMPORTED_OBJECT_MAX_RETRIES = 5.freeze + + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze + def self.create(*args) new(*args).create end @@ -22,24 +26,35 @@ module Gitlab @relation_hash = relation_hash.except('id', 'noteable_id') @members_mapper = members_mapper @user = user + @imported_object_retries = 0 end # Creates an object from an actual model with name "relation_sym" with params from # the relation_hash, updating references with new object IDs, mapping users using # the "members_mapper" object, also updating notes if required. def create - set_note_author if @relation_name == :notes + setup_models + + generate_imported_object + end + + private + + def setup_models + if @relation_name == :notes + set_note_author + + # attachment is deprecated and note uploads are handled by Markdown uploader + @relation_hash['attachment'] = nil + end + update_user_references update_project_references reset_ci_tokens if @relation_name == 'Ci::Trigger' @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] set_st_diffs if @relation_name == :merge_request_diff - - generate_imported_object end - private - def update_user_references USER_REFERENCES.each do |reference| if @relation_hash[reference] @@ -112,10 +127,14 @@ module Gitlab end def imported_object - imported_object = relation_class.new(parsed_relation_hash) - yield(imported_object) if block_given? - imported_object.importing = true if imported_object.respond_to?(:importing) - imported_object + yield(existing_or_new_object) if block_given? + existing_or_new_object.importing = true if existing_or_new_object.respond_to?(:importing) + existing_or_new_object + rescue ActiveRecord::RecordNotUnique + # as the operation is not atomic, retry in the unlikely scenario an INSERT is + # performed on the same object between the SELECT and the INSERT + @imported_object_retries += 1 + retry if @imported_object_retries < IMPORTED_OBJECT_MAX_RETRIES end def update_note_for_missing_author(author_name) @@ -134,6 +153,20 @@ module Gitlab def set_st_diffs @relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs') end + + def existing_or_new_object + # Only find existing records to avoid mapping tables such as milestones + # Otherwise always create the record, skipping the extra SELECT clause. + @existing_or_new_object ||= begin + if EXISTING_OBJECT_CHECK.include?(@relation_name) + existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id')) + existing_object.assign_attributes(parsed_relation_hash) + existing_object + else + relation_class.new(parsed_relation_hash) + end + end + end end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index f84de652a57..6d9379acf25 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -14,7 +14,7 @@ module Gitlab FileUtils.mkdir_p(path_to_repo) - git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) + git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks rescue => e @shared.error(e) false @@ -29,6 +29,16 @@ module Gitlab def path_to_repo @project.repository.path_to_repo end + + def repo_restore_hooks + return true if wiki? + + git_restore_hooks + end + + def wiki? + @project.class.name == 'ProjectWiki' + end end end end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 8ce9d32abe0..d7be50bd437 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,7 +1,7 @@ module Gitlab module IncomingEmail class << self - FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze + FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze def enabled? config.enabled && config.address @@ -21,8 +21,8 @@ module Gitlab match[1] end - def key_from_fallback_reply_message_id(message_id) - match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX) + def key_from_fallback_message_id(mail_id) + match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX) return unless match match[1] diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 49f702f91f6..41fcd971c22 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -124,6 +124,11 @@ module Gitlab trans.action = action if trans end + # Returns the prefix to use for the name of a series. + def self.series_prefix + @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_' + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? @@ -136,8 +141,7 @@ module Gitlab end end - private - + # Allow access from other metrics related middlewares def self.current_transaction Transaction.current end diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index dcec7543c13..4b7a791e497 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -9,14 +9,17 @@ module Gitlab # # Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login) module Instrumentation - SERIES = 'method_calls' - PROXY_IVAR = :@__gitlab_instrumentation_proxy def self.configure yield self end + # Returns the name of the series to use for storing method calls. + def self.series + @series ||= "#{Metrics.series_prefix}method_calls" + end + # Instruments a class method. # # mod - The module to instrument as a Module/Class. @@ -141,15 +144,15 @@ module Gitlab # generated method _only_ accepts regular arguments if the underlying # method also accepts them. if method.arity == 0 - args_signature = '&block' + args_signature = '' else - args_signature = '*args, &block' + args_signature = '*args' end proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1 def #{name}(#{args_signature}) if trans = Gitlab::Metrics::Instrumentation.transaction - trans.measure_method(#{label.inspect}) { super } + trans.method_call_for(#{label.to_sym.inspect}).measure { super } else super end diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb index c048fe20ba7..d3465e5ec19 100644 --- a/lib/gitlab/metrics/method_call.rb +++ b/lib/gitlab/metrics/method_call.rb @@ -11,8 +11,8 @@ module Gitlab def initialize(name, series) @name = name @series = series - @real_time = 0.0 - @cpu_time = 0.0 + @real_time = 0 + @cpu_time = 0 @call_count = 0 end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index 82c18bb108b..287b7a83547 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -35,12 +35,12 @@ module Gitlab if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID) def self.cpu_time Process. - clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond).to_f + clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) end else def self.cpu_time Process. - clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond).to_f + clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) end end @@ -48,14 +48,14 @@ module Gitlab # # Returns the time as a Float. def self.real_time(precision = :millisecond) - Process.clock_gettime(Process::CLOCK_REALTIME, precision).to_f + Process.clock_gettime(Process::CLOCK_REALTIME, precision) end # Returns the current monotonic clock time in a given precision. # # Returns the time as a Float. def self.monotonic_time(precision = :millisecond) - Process.clock_gettime(Process::CLOCK_MONOTONIC, precision).to_f + Process.clock_gettime(Process::CLOCK_MONOTONIC, precision) end end end diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index bded245da43..968f3218950 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -52,23 +52,16 @@ module Gitlab end def add_metric(series, values, tags = {}) - @metrics << Metric.new("#{series_prefix}#{series}", values, tags) + @metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags) end - # Measures the time it takes to execute a method. - # - # Multiple calls to the same method add up to the total runtime of the - # method. - # - # name - The full name of the method to measure (e.g. `User#sign_in`). - def measure_method(name, &block) - unless @methods[name] - series = "#{series_prefix}#{Instrumentation::SERIES}" - - @methods[name] = MethodCall.new(name, series) + # Returns a MethodCall object for the given name. + def method_call_for(name) + unless method = @methods[name] + @methods[name] = method = MethodCall.new(name, Instrumentation.series) end - @methods[name].measure(&block) + method end def increment(name, value) @@ -115,14 +108,6 @@ module Gitlab Metrics.submit_metrics(submit_hashes) end - - def sidekiq? - Sidekiq.server? - end - - def series_prefix - sidekiq? ? 'sidekiq_' : 'rails_' - end end end end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 8da8b754975..4e787dc0656 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,4 +1,5 @@ require 'ruby-prof' +require 'gitlab/request_profiler' module Gitlab module RequestProfiler @@ -28,7 +29,9 @@ module Gitlab def call_with_profiling(env) ret = nil result = RubyProf::Profile.profile do - ret = @app.call(env) + ret = catch(:warden) do + @app.call(env) + end end printer = RubyProf::CallStackPrinter.new(result) @@ -40,7 +43,11 @@ module Gitlab printer.print(file) end - ret + if ret.is_a?(Array) + ret + else + throw(:warden, ret) + end end end end diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index 83f91de810c..d4020af76f9 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -2,6 +2,8 @@ module Gitlab # Module containing GitLab's application theme definitions and helper methods # for accessing them. module Themes + extend self + # Theme ID used when no `default_theme` configuration setting is provided. APPLICATION_DEFAULT = 2 @@ -22,7 +24,7 @@ module Gitlab # classes that might be applied to the `body` element # # Returns a String - def self.body_classes + def body_classes THEMES.collect(&:css_class).uniq.join(' ') end @@ -33,26 +35,26 @@ module Gitlab # id - Integer ID # # Returns a Theme - def self.by_id(id) + def by_id(id) THEMES.detect { |t| t.id == id } || default end # Returns the number of defined Themes - def self.count + def count THEMES.size end # Get the default Theme # # Returns a Theme - def self.default + def default by_id(default_id) end # Iterate through each Theme # # Yields the Theme object - def self.each(&block) + def each(&block) THEMES.each(&block) end @@ -61,7 +63,7 @@ module Gitlab # user - User record # # Returns a Theme - def self.for_user(user) + def for_user(user) if user by_id(user.theme_id) else @@ -71,7 +73,7 @@ module Gitlab private - def self.default_id + def default_id id = Gitlab.config.gitlab.default_theme.to_i # Prevent an invalid configuration setting from causing an infinite loop diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index c0f85e9b3a8..3a69027368f 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -29,8 +29,9 @@ module Gitlab def can_push_to_branch?(ref) return false unless user - if project.protected_branch?(ref) && !project.developers_can_push_to_protected_branch?(ref) - user.can?(:push_code_to_protected_branches, project) + if project.protected_branch?(ref) + access_levels = project.protected_branches.matching(ref).map(&:push_access_level) + access_levels.any? { |access_level| access_level.check_access(user) } else user.can?(:push_code, project) end @@ -39,8 +40,9 @@ module Gitlab def can_merge_to_branch?(ref) return false unless user - if project.protected_branch?(ref) && !project.developers_can_merge_to_protected_branch?(ref) - user.can?(:push_code_to_protected_branches, project) + if project.protected_branch?(ref) + access_levels = project.protected_branches.matching(ref).map(&:merge_access_level) + access_levels.any? { |access_level| access_level.check_access(user) } else user.can?(:push_code, project) end diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index f818dc78d34..4edfd015074 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -18,7 +18,7 @@ module Rouge is_first = false yield %(<span id="LC#{@line_number}" class="line">) - line.each { |token, value| yield span(token, value) } + line.each { |token, value| yield span(token, value.chomp) } yield %(</span>) @line_number += 1 diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake index 30a2e9be5ce..afe5d42910c 100644 --- a/lib/tasks/downtime_check.rake +++ b/lib/tasks/downtime_check.rake @@ -1,26 +1,12 @@ desc 'Checks if migrations in a branch require downtime' task downtime_check: :environment do - # First we'll want to make sure we're comparing with the right upstream - # repository/branch. - current_branch = `git rev-parse --abbrev-ref HEAD`.strip - - # Either the developer ran this task directly on the master branch, or they're - # making changes directly on the master branch. - if current_branch == 'master' - if defined?(Gitlab::License) - repo = 'gitlab-ee' - else - repo = 'gitlab-ce' - end - - `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1` - - compare_with = 'FETCH_HEAD' - # The developer is working on a different branch, in this case we can just - # compare with the master branch. + if defined?(Gitlab::License) + repo = 'gitlab-ee' else - compare_with = 'master' + repo = 'gitlab-ce' end - Rake::Task['gitlab:db:downtime_check'].invoke(compare_with) + `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1` + + Rake::Task['gitlab:db:downtime_check'].invoke('FETCH_HEAD') end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 3001d32e719..df902da86f8 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -24,15 +24,6 @@ describe Projects::CommitController do get :show, params.merge(extra_params) end - let(:project) { create(:project) } - - before do - user = create(:user) - project.team << [user, :master] - - sign_in(user) - end - context 'with valid id' do it 'responds with 200' do go(id: commit.id) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb new file mode 100644 index 00000000000..768105cae95 --- /dev/null +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Projects::EnvironmentsController do + let(:environment) { create(:environment) } + let(:project) { environment.project } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + + sign_in(user) + end + + describe 'GET show' do + context 'with valid id' do + it 'responds with a status code 200' do + get :show, environment_params + + expect(response).to be_ok + end + end + + context 'with invalid id' do + it 'responds with a status code 404' do + params = environment_params + params[:id] = 12345 + get :show, params + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET edit' do + it 'responds with a status code 200' do + get :edit, environment_params + + expect(response).to be_ok + end + end + + describe 'PATCH #update' do + it 'responds with a 302' do + patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' }) + patch :update, patch_params + + expect(response).to have_http_status(302) + end + end + + def environment_params + { + namespace_id: project.namespace, + project_id: project, + id: environment.id + } + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 7cf09fa4a4a..77f65057f71 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -243,6 +243,37 @@ describe Projects::IssuesController do end end + describe 'POST #create' do + context 'Akismet is enabled' do + before do + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) + end + + def post_spam_issue + sign_in(user) + spam_project = create(:empty_project, :public) + post :create, { + namespace_id: spam_project.namespace.to_param, + project_id: spam_project.to_param, + issue: { title: 'Spam Title', description: 'Spam lives here' } + } + end + + it 'rejects an issue recognized as spam' do + expect{ post_spam_issue }.not_to change(Issue, :count) + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + post_spam_issue + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('Spam Title') + end + end + end + describe "DELETE #destroy" do context "when the user is a developer" do before { sign_in(user) } diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 5e19e403c6b..1b32d560b16 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -90,5 +90,21 @@ FactoryGirl.define do build.save! end end + + trait :artifacts_expired do + after(:create) do |build, _| + build.artifacts_file = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), + 'application/zip') + + build.artifacts_metadata = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), + 'application/x-gzip') + + build.artifacts_expire_at = 1.minute.ago + + build.save! + end + end end end diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index 07265c26ca3..846cccfc7fa 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -3,5 +3,6 @@ FactoryGirl.define do sequence(:name) { |n| "environment#{n}" } project factory: :empty_project + sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" } end end diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index 28ed8078157..5575852c2d7 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -2,5 +2,28 @@ FactoryGirl.define do factory :protected_branch do name project + + after(:create) do |protected_branch| + protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER) + protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER) + end + + trait :developers_can_push do + after(:create) do |protected_branch| + protected_branch.push_access_level.update!(access_level: Gitlab::Access::DEVELOPER) + end + end + + trait :developers_can_merge do + after(:create) do |protected_branch| + protected_branch.merge_access_level.update!(access_level: Gitlab::Access::DEVELOPER) + end + end + + trait :no_one_can_push do + after(:create) do |protected_branch| + protected_branch.push_access_level.update!(access_level: Gitlab::Access::NO_ACCESS) + end + end end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index a7d9f2a0c72..fcd41b38413 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -140,7 +140,7 @@ feature 'Environments', feature: true do context 'for valid name' do before do fill_in('Name', with: 'production') - click_on 'Create environment' + click_on 'Save' end scenario 'does create a new pipeline' do @@ -151,7 +151,7 @@ feature 'Environments', feature: true do context 'for invalid name' do before do fill_in('Name', with: 'name with spaces') - click_on 'Create environment' + click_on 'Save' end scenario 'does show errors' do diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb new file mode 100644 index 00000000000..0d495cd04aa --- /dev/null +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe 'Projects > Issuables > Default sort order', feature: true do + let(:project) { create(:empty_project, :public) } + + let(:first_created_issuable) { issuables.order_created_asc.first } + let(:last_created_issuable) { issuables.order_created_desc.first } + + let(:first_updated_issuable) { issuables.order_updated_asc.first } + let(:last_updated_issuable) { issuables.order_updated_desc.first } + + context 'for merge requests' do + include MergeRequestHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + source_branch: "#{issuable_type}_#{i}", + source_project: project }.merge(ts) + end + + MergeRequest.all + end + + context 'in the "merge requests" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests project + + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / open" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests_with_state(project, 'open') + + expect(selected_sort_order).to eq('last created') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / merged" tab', js: true do + let(:issuable_type) { :merged_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'merged') + + expect(selected_sort_order).to eq('last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / closed" tab', js: true do + let(:issuable_type) { :closed_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'closed') + + expect(selected_sort_order).to eq('last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / all" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests_with_state(project, 'all') + + expect(selected_sort_order).to eq('last created') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + end + + context 'for issues' do + include IssueHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + project: project }.merge(ts) + end + + Issue.all + end + + context 'in the "issues" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues project + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / open" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues_with_state(project, 'open') + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / closed" tab', js: true do + let(:issuable_type) { :closed_issue } + + it 'is "last updated"' do + visit_issues_with_state(project, 'closed') + + expect(selected_sort_order).to eq('last updated') + expect(first_issue).to include(last_updated_issuable.title) + expect(last_issue).to include(first_updated_issuable.title) + end + end + + context 'in the "issues / all" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues_with_state(project, 'all') + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + end + + def selected_sort_order + find('.pull-right .dropdown button').text.downcase + end + + def visit_merge_requests_with_state(project, state) + visit_merge_requests project + visit_issuables_with_state state + end + + def visit_issues_with_state(project, state) + visit_issues project + visit_issuables_with_state state + end + + def visit_issuables_with_state(state) + within('.issues-state-filters') { find("span", text: state.titleize).click } + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d51c9abea19..9c92b52898c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include IssueHelpers include SortingHelper let(:project) { create(:project) } @@ -186,15 +187,15 @@ describe 'Issues', feature: true do it 'sorts by newest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by oldest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - expect(first_issue).to include('foo') - expect(last_issue).to include('baz') + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by most recently updated' do @@ -350,8 +351,8 @@ describe 'Issues', feature: true do sort: sort_value_oldest_created, assignee_id: user2.id) - expect(first_issue).to include('foo') - expect(last_issue).to include('bar') + expect(first_issue).to include('bar') + expect(last_issue).to include('foo') expect(page).not_to have_content 'baz' end end @@ -524,6 +525,35 @@ describe 'Issues', feature: true do end end + describe 'new issue by email' do + shared_examples 'show the email in the modal' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + + visit namespace_project_issues_path(project.namespace, project) + click_button('Email a new issue') + end + + it 'click the button to show modal for the new email' do + page.within '#issue-email-modal' do + email = project.new_issue_address(@user) + + expect(page).to have_selector("input[value='#{email}']") + end + end + end + + context 'with existing issues' do + let!(:issue) { create(:issue, project: project, author: @user) } + + it_behaves_like 'show the email in the modal' + end + + context 'without existing issues' do + it_behaves_like 'show the email in the modal' + end + end + describe 'due date' do context 'update due on issue#show', js: true do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } @@ -561,14 +591,6 @@ describe 'Issues', feature: true do end end - def first_issue - page.all('ul.issues-list > li').first.text - end - - def last_issue - page.all('ul.issues-list > li').last.text - end - def drop_in_dropzone(file_path) # Generate a fake input selector page.execute_script <<-JS diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 1c130057c56..cabb8e455f9 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Projects > Merge requests > User lists merge requests', feature: true do + include MergeRequestHelpers include SortingHelper let(:project) { create(:project, :public) } @@ -23,10 +24,12 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true milestone: create(:milestone, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) + # lfs in itself is not a great choice for the title if one wants to match the whole body content later on + # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name create(:merge_request, - title: 'lfs', + title: 'merge_lfs', source_project: project, - source_branch: 'lfs', + source_branch: 'merge_lfs', created_at: 3.minutes.ago, updated_at: 10.seconds.ago) end @@ -35,7 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true visit_merge_requests(project, assignee_id: IssuableFinder::NONE) expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project)) - expect(page).to have_content 'lfs' + expect(page).to have_content 'merge_lfs' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' expect(count_merge_requests).to eq(1) @@ -44,7 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true it 'filters on a specific assignee' do visit_merge_requests(project, assignee_id: user.id) - expect(page).not_to have_content 'lfs' + expect(page).not_to have_content 'merge_lfs' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' expect(count_merge_requests).to eq(2) @@ -53,23 +56,23 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true it 'sorts by newest' do visit_merge_requests(project, sort: sort_value_recently_created) - expect(first_merge_request).to include('lfs') - expect(last_merge_request).to include('fix') + expect(first_merge_request).to include('fix') + expect(last_merge_request).to include('merge_lfs') expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do visit_merge_requests(project, sort: sort_value_oldest_created) - expect(first_merge_request).to include('fix') - expect(last_merge_request).to include('lfs') + expect(first_merge_request).to include('merge_lfs') + expect(last_merge_request).to include('fix') expect(count_merge_requests).to eq(3) end it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) - expect(first_merge_request).to include('lfs') + expect(first_merge_request).to include('merge_lfs') expect(count_merge_requests).to eq(3) end @@ -143,18 +146,6 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true end end - def visit_merge_requests(project, opts = {}) - visit namespace_project_merge_requests_path(project.namespace, project, opts) - end - - def first_merge_request - page.all('ul.mr-list > li').first.text - end - - def last_merge_request - page.all('ul.mr-list > li').last.text - end - def count_merge_requests page.all('ul.mr-list > li').count end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 7f861db1969..377a9aba60d 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -116,9 +116,19 @@ describe "Pipelines" do it { expect(page).to have_link(with_artifacts.name) } end + context 'with artifacts expired' do + let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + context 'without artifacts' do let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + before { visit namespace_project_pipelines_path(project.namespace, project) } + it { expect(page).not_to have_selector('.build-artifacts') } end end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 2d1e3bbebe5..7835e1678ad 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -8,6 +8,7 @@ feature 'project import', feature: true, js: true do let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } let(:project) { Project.last } + let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -37,7 +38,7 @@ feature 'project import', feature: true, js: true do expect(project).not_to be_nil expect(project.issues).not_to be_empty expect(project.merge_requests).not_to be_empty - expect(project.repo_exists?).to be true + expect(project_hook).to exist expect(wiki_exists?).to be true expect(project.import_status).to eq('finished') end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index d94dee0c797..57734b33a44 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Projected Branches', feature: true, js: true do + include WaitForAjax + let(:user) { create(:user, :admin) } let(:project) { create(:project) } @@ -81,4 +83,68 @@ feature 'Projected Branches', feature: true, js: true do end end end + + describe "access control" do + ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can push to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + find(".allowed-to-push").click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id) + end + + it "allows updating protected branches so that #{access_type_name} can push to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".allowed-to-push").click + within('.dropdown-menu.push') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id) + end + end + + ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can merge to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + find(".allowed-to-merge").click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id) + end + + it "allows updating protected branches so that #{access_type_name} can merge to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".allowed-to-merge").click + within('.dropdown-menu.merge') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id) + end + end + end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index d0a301038c4..09f70cd3b00 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -28,6 +28,26 @@ describe "Search", feature: true do end context 'search for comments' do + context 'when comment belongs to a invalid commit' do + let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } + + before { note.update_attributes(commit_id: 12345678) } + + it 'finds comment' do + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: note.note + click_button 'Go' + end + + click_link 'Comments' + + expect(page).to have_text("Commit deleted") + expect(page).to have_text("12345678") + end + end + it 'finds a snippet' do snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') note = create(:note, diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index dd85203a038..482caeee64a 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -16,15 +16,15 @@ describe BranchesFinder do end it 'sorts by recently_updated' do - branches_finder = described_class.new(repository, { sort: 'recently_updated' }) + branches_finder = described_class.new(repository, { sort: 'updated_desc' }) result = branches_finder.execute - expect(result.first.name).to eq('video') + expect(result.first.name).to eq('crlf-diff') end it 'sorts by last_updated' do - branches_finder = described_class.new(repository, { sort: 'last_updated' }) + branches_finder = described_class.new(repository, { sort: 'updated_asc' }) result = branches_finder.execute @@ -53,7 +53,7 @@ describe BranchesFinder do context 'filter and sort' do it 'filters branches by name and sorts by recently_updated' do - params = { sort: 'recently_updated', search: 'feature' } + params = { sort: 'updated_desc', search: 'feature' } branches_finder = described_class.new(repository, params) result = branches_finder.execute @@ -63,7 +63,7 @@ describe BranchesFinder do end it 'filters branches by name and sorts by last_updated' do - params = { sort: 'last_updated', search: 'feature' } + params = { sort: 'updated_asc', search: 'feature' } branches_finder = described_class.new(repository, params) result = branches_finder.execute diff --git a/spec/fixtures/emails/valid_new_issue.eml b/spec/fixtures/emails/valid_new_issue.eml new file mode 100644 index 00000000000..3cf53a656a5 --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue.eml @@ -0,0 +1,23 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +The reply by email functionality should be extended to allow creating a new issue by email. + +* Allow an admin to specify which project the issue should be created under by checking the sender domain. +* Possibly allow the use of regular expression matches within the subject/body to specify which project the issue should be created under. diff --git a/spec/fixtures/emails/valid_new_issue_empty.eml b/spec/fixtures/emails/valid_new_issue_empty.eml new file mode 100644 index 00000000000..fc1d52a3f42 --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue_empty.eml @@ -0,0 +1,18 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_authentication_token.eml new file mode 100644 index 00000000000..0994c2f7775 --- /dev/null +++ b/spec/fixtures/emails/wrong_authentication_token.eml @@ -0,0 +1,18 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+bad_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/emails/wrong_reply_key.eml b/spec/fixtures/emails/wrong_mail_key.eml index 491e078fb5b..491e078fb5b 100644 --- a/spec/fixtures/emails/wrong_reply_key.eml +++ b/spec/fixtures/emails/wrong_mail_key.eml diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index bd0108f9938..b2d6d59b1ee 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BlobHelper do + include TreeHelper + let(:blob_name) { 'test.lisp' } let(:no_context_content) { ":type \"assem\"))" } let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } @@ -65,4 +67,20 @@ describe BlobHelper do expect(sanitize_svg(blob).data).to eq(expected) end end + + describe "#edit_blob_link" do + let(:project) { create(:project) } + + before do + allow(self).to receive(:current_user).and_return(double) + end + + it 'verifies blob is text' do + expect(self).not_to receive(:blob_text_viewable?) + + button = edit_blob_link(project, 'refs/heads/master', 'README.md') + + expect(button).to start_with('<button') + end + end end diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb index 52d5a7dffc9..290e47763eb 100644 --- a/spec/initializers/trusted_proxies_spec.rb +++ b/spec/initializers/trusted_proxies_spec.rb @@ -47,6 +47,12 @@ describe 'trusted_proxies', lib: true do expect(request.remote_ip).to eq('1.1.1.1') expect(request.ip).to eq('1.1.1.1') end + + it 'handles invalid ip addresses' do + request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 1.1.1.1:12345, 1.1.1.1') + expect(request.remote_ip).to eq('1.1.1.1') + expect(request.ip).to eq('1.1.1.1') + end end def stub_request(headers = {}) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 9921171f2aa..224baca8030 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -78,12 +78,24 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do end context 'with a valid repository' do + it 'rebuilds absolute URL for a file in the repo' do + doc = filter(link('/doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo' do doc = filter(link('doc/api/README.md')) expect(doc.at_css('a')['href']). to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end + it 'rebuilds relative URL for a file in the repo with leading ./' do + doc = filter(link('./doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo up one directory' do relative_link = link('../api/README.md') doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 514c752546d..85cfe728b6a 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do end it 'returns the nodes when the user can read the issue' do - expect(Ability.abilities).to receive(:allowed?). - with(user, :read_issue, issue). - and_return(true) + expect(Ability).to receive(:issues_readable_by_user). + with([issue], user). + and_return([issue]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end it 'returns an empty Array when the user can not read the issue' do - expect(Ability.abilities).to receive(:allowed?). - with(user, :read_issue, issue). - and_return(false) + expect(Ability).to receive(:issues_readable_by_user). + with([issue], user). + and_return([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index d20fd4ab7dd..61490555ff5 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -162,7 +162,7 @@ module Ci shared_examples 'raises an error' do it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps') + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps') end end @@ -318,7 +318,7 @@ module Ci shared_examples 'raises an error' do it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps') + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps') end end @@ -559,7 +559,7 @@ module Ci it 'raises error' do expect { subject } .to raise_error(GitlabCiYamlProcessor::ValidationError, - /job: variables should be a map/) + /jobs:rspec:variables config should be a hash of key value pairs/) end end @@ -774,7 +774,7 @@ module Ci let(:environment) { 1 } it 'raises error' do - expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}") + expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") end end @@ -782,7 +782,7 @@ module Ci let(:environment) { 'production staging' } it 'raises error' do - expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}") + expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") end end end @@ -973,7 +973,7 @@ EOT config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") end it "returns errors if before_script parameter is invalid" do @@ -987,7 +987,7 @@ EOT config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") end it "returns errors if after_script parameter is invalid" do @@ -1001,7 +1001,7 @@ EOT config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") end it "returns errors if image parameter is invalid" do @@ -1015,21 +1015,21 @@ EOT config = YAML.dump({ '' => { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank") end it "returns errors if job name is non-string" do config = YAML.dump({ 10 => { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol") end it "returns errors if job image parameter is invalid" do config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string") end it "returns errors if services parameter is not an array" do @@ -1050,49 +1050,56 @@ EOT config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") end - it "returns errors if there are unknown parameters" do + it "returns error if job configuration is invalid" do config = YAML.dump({ extra: "bundle update" }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") end it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do config = YAML.dump({ extra: { services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings") end it "returns errors if there are no jobs defined" do config = YAML.dump({ before_script: ["bundle update"] }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if there are no visible jobs defined" do + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) + expect do + GitlabCiYamlProcessor.new(config, path) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") end it "returns errors if job allow_failure parameter is not an boolean" do config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") end it "returns errors if job stage is not a string" do config = YAML.dump({ rspec: { script: "test", type: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string") end it "returns errors if job stage is not a pre-defined stage" do @@ -1141,49 +1148,49 @@ EOT config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure, always or manual") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") end it "returns errors if job artifacts:name is not an a string" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") end it "returns errors if job artifacts:when is not an a predefined value" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") end it "returns errors if job artifacts:expire_in is not an a string" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:expire_in is not an a valid duration" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") end it "returns errors if job artifacts:paths is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") end it "returns errors if cache:untracked is not an array of strings" do @@ -1211,28 +1218,28 @@ EOT config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") end it "returns errors if job cache:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") end it "returns errors if job cache:paths is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") end it "returns errors if job dependencies is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb index 88a71528867..b08396da4d2 100644 --- a/spec/lib/gitlab/akismet_helper_spec.rb +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::AkismetHelper, type: :helper do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } before do @@ -11,13 +11,13 @@ describe Gitlab::AkismetHelper, type: :helper do end describe '#check_for_spam?' do - it 'returns true for non-member' do - expect(helper.check_for_spam?(project, user)).to eq(true) + it 'returns true for public project' do + expect(helper.check_for_spam?(project)).to eq(true) end - it 'returns false for member' do - project.team << [user, :guest] - expect(helper.check_for_spam?(project, user)).to eq(false) + it 'returns false for private project' do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + expect(helper.check_for_spam?(project)).to eq(false) end end diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb new file mode 100644 index 00000000000..c09a0a9c793 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Artifacts do + let(:entry) { described_class.new(config) } + + describe 'validation' do + context 'when entry config value is correct' do + let(:config) { { paths: %w[public/] } } + + describe '#value' do + it 'returns artifacs configuration' do + expect(entry.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'when value of attribute is invalid' do + let(:config) { { name: 10 } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts name should be a string' + end + end + + context 'when there is an unknown key present' do + let(:config) { { test: 100 } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts config contains unknown keys: test' + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/node/attributable_spec.rb new file mode 100644 index 00000000000..24d9daafd88 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/attributable_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Attributable do + let(:node) { Class.new } + let(:instance) { node.new } + + before do + node.include(described_class) + + node.class_eval do + attributes :name, :test + end + end + + context 'config is a hash' do + before do + allow(instance) + .to receive(:config) + .and_return({ name: 'some name', test: 'some test' }) + end + + it 'returns the value of config' do + expect(instance.name).to eq 'some name' + expect(instance.test).to eq 'some test' + end + + it 'returns no method error for unknown attributes' do + expect { instance.unknown }.to raise_error(NoMethodError) + end + end + + context 'config is not a hash' do + before do + allow(instance) + .to receive(:config) + .and_return('some test') + end + + it 'returns nil' do + expect(instance.test).to be_nil + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/commands_spec.rb b/spec/lib/gitlab/ci/config/node/commands_spec.rb new file mode 100644 index 00000000000..e373c40706f --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/commands_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Commands do + let(:entry) { described_class.new(config) } + + context 'when entry config value is an array' do + let(:config) { ['ls', 'pwd'] } + + describe '#value' do + it 'returns array of strings' do + expect(entry.value).to eq config + end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + end + + context 'when entry config value is a string' do + let(:config) { 'ls' } + + describe '#value' do + it 'returns array with single element' do + expect(entry.value).to eq ['ls'] + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not valid' do + let(:config) { 1 } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'commands config should be a ' \ + 'string or an array of strings' + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index 91ddef7bfbf..d26185ba585 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Factory do describe '#create!' do - let(:factory) { described_class.new(entry_class) } - let(:entry_class) { Gitlab::Ci::Config::Node::Script } + let(:factory) { described_class.new(node) } + let(:node) { Gitlab::Ci::Config::Node::Script } - context 'when setting up a value' do + context 'when setting a concrete value' do it 'creates entry with valid value' do entry = factory - .with(value: ['ls', 'pwd']) + .value(['ls', 'pwd']) .create! expect(entry.value).to eq ['ls', 'pwd'] @@ -17,7 +17,7 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when setting description' do it 'creates entry with description' do entry = factory - .with(value: ['ls', 'pwd']) + .value(['ls', 'pwd']) .with(description: 'test description') .create! @@ -29,7 +29,8 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when setting key' do it 'creates entry with custom key' do entry = factory - .with(value: ['ls', 'pwd'], key: 'test key') + .value(['ls', 'pwd']) + .with(key: 'test key') .create! expect(entry.key).to eq 'test key' @@ -37,19 +38,20 @@ describe Gitlab::Ci::Config::Node::Factory do end context 'when setting a parent' do - let(:parent) { Object.new } + let(:object) { Object.new } it 'creates entry with valid parent' do entry = factory - .with(value: 'ls', parent: parent) + .value('ls') + .with(parent: object) .create! - expect(entry.parent).to eq parent + expect(entry.parent).to eq object end end end - context 'when not setting up a value' do + context 'when not setting a value' do it 'raises error' do expect { factory.create! }.to raise_error( Gitlab::Ci::Config::Node::Factory::InvalidFactory @@ -60,11 +62,25 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when creating entry with nil value' do it 'creates an undefined entry' do entry = factory - .with(value: nil) + .value(nil) .create! expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined end end + + context 'when passing metadata' do + let(:node) { spy('node') } + + it 'passes metadata as a parameter' do + factory + .value('some value') + .metadata(some: 'hash') + .create! + + expect(node).to have_received(:new) + .with('some value', { some: 'hash' }) + end + end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index c87c9e97bc8..2f87d270b36 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -22,38 +22,40 @@ describe Gitlab::Ci::Config::Node::Global do variables: { VAR: 'value' }, after_script: ['make clean'], stages: ['build', 'pages'], - cache: { key: 'k', untracked: true, paths: ['public/'] } } + cache: { key: 'k', untracked: true, paths: ['public/'] }, + rspec: { script: %w[rspec ls] }, + spinach: { script: 'spinach' } } end describe '#process!' do before { global.process! } it 'creates nodes hash' do - expect(global.nodes).to be_an Array + expect(global.descendants).to be_an Array end it 'creates node object for each entry' do - expect(global.nodes.count).to eq 8 + expect(global.descendants.count).to eq 8 end it 'creates node object using valid class' do - expect(global.nodes.first) + expect(global.descendants.first) .to be_an_instance_of Gitlab::Ci::Config::Node::Script - expect(global.nodes.second) + expect(global.descendants.second) .to be_an_instance_of Gitlab::Ci::Config::Node::Image end it 'sets correct description for nodes' do - expect(global.nodes.first.description) + expect(global.descendants.first.description) .to eq 'Script that will be executed before each job.' - expect(global.nodes.second.description) + expect(global.descendants.second.description) .to eq 'Docker image that will be used to execute jobs.' end - end - describe '#leaf?' do - it 'is not leaf' do - expect(global).not_to be_leaf + describe '#leaf?' do + it 'is not leaf' do + expect(global).not_to be_leaf + end end end @@ -63,6 +65,12 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.before_script).to be nil end end + + describe '#leaf?' do + it 'is leaf' do + expect(global).to be_leaf + end + end end context 'when processed' do @@ -106,7 +114,10 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when deprecated types key defined' do - let(:hash) { { types: ['test', 'deploy'] } } + let(:hash) do + { types: ['test', 'deploy'], + rspec: { script: 'rspec' } } + end it 'returns array of types as stages' do expect(global.stages).to eq %w[test deploy] @@ -120,20 +131,33 @@ describe Gitlab::Ci::Config::Node::Global do .to eq(key: 'k', untracked: true, paths: ['public/']) end end + + describe '#jobs' do + it 'returns jobs configuration' do + expect(global.jobs).to eq( + rspec: { name: :rspec, + script: %w[rspec ls], + stage: 'test' }, + spinach: { name: :spinach, + script: %w[spinach], + stage: 'test' } + ) + end + end end end context 'when most of entires not defined' do - let(:hash) { { cache: { key: 'a' }, rspec: {} } } + let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } } before { global.process! } describe '#nodes' do it 'instantizes all nodes' do - expect(global.nodes.count).to eq 8 + expect(global.descendants.count).to eq 8 end it 'contains undefined nodes' do - expect(global.nodes.first) + expect(global.descendants.first) .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined end end @@ -164,7 +188,7 @@ describe Gitlab::Ci::Config::Node::Global do # details. # context 'when entires specified but not defined' do - let(:hash) { { variables: nil } } + let(:hash) { { variables: nil, rspec: { script: 'rspec' } } } before { global.process! } describe '#variables' do @@ -196,10 +220,8 @@ describe Gitlab::Ci::Config::Node::Global do end describe '#before_script' do - it 'raises error' do - expect { global.before_script }.to raise_error( - Gitlab::Ci::Config::Node::Entry::InvalidError - ) + it 'returns nil' do + expect(global.before_script).to be_nil end end end @@ -220,9 +242,9 @@ describe Gitlab::Ci::Config::Node::Global do end end - describe '#defined?' do + describe '#specified?' do it 'is concrete entry that is defined' do - expect(global.defined?).to be true + expect(global.specified?).to be true end end end diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb new file mode 100644 index 00000000000..cc44e2cc054 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::HiddenJob do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { image: 'ruby:2.2' } } + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(image: 'ruby:2.2') + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'hidden job config should be a hash' + end + end + end + + context 'when config is empty' do + let(:config) { {} } + + describe '#valid' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end + end + end + + describe '#leaf?' do + it 'is a leaf' do + expect(entry).to be_leaf + end + end + + describe '#relevant?' do + it 'is not a relevant entry' do + expect(entry).not_to be_relevant + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb new file mode 100644 index 00000000000..1484fb60dd8 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Job do + let(:entry) { described_class.new(config, name: :rspec) } + + before { entry.process! } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { script: 'rspec' } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when job name is empty' do + let(:entry) { described_class.new(config, name: ''.to_sym) } + + it 'reports error' do + expect(entry.errors) + .to include "job name can't be blank" + end + end + end + + context 'when entry value is not correct' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } + + describe '#errors' do + it 'reports error about a config type' do + expect(entry.errors) + .to include 'job config should be a hash' + end + end + end + + context 'when config is empty' do + let(:config) { {} } + + describe '#valid' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unknown keys detected' do + let(:config) { { unknown: true } } + + describe '#valid' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + end + end + + describe '#value' do + context 'when entry is correct' do + let(:config) do + { before_script: %w[ls pwd], + script: 'rspec', + after_script: %w[cleanup] } + end + + it 'returns correct value' do + expect(entry.value) + .to eq(name: :rspec, + before_script: %w[ls pwd], + script: %w[rspec], + stage: 'test', + after_script: %w[cleanup]) + end + end + end + + describe '#relevant?' do + it 'is a relevant entry' do + expect(entry).to be_relevant + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb new file mode 100644 index 00000000000..b8d9c70479c --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Jobs do + let(:entry) { described_class.new(config) } + + describe 'validations' do + before { entry.process! } + + context 'when entry config value is correct' do + let(:config) { { rspec: { script: 'rspec' } } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } + + it 'returns error about incorrect type' do + expect(entry.errors) + .to include 'jobs config should be a hash' + end + end + + context 'when job is unspecified' do + let(:config) { { rspec: nil } } + + it 'reports error' do + expect(entry.errors).to include "rspec config can't be blank" + end + end + + context 'when no visible jobs present' do + let(:config) { { '.hidden'.to_sym => { script: [] } } } + + it 'returns error about no visible jobs defined' do + expect(entry.errors) + .to include 'jobs config should contain at least one visible job' + end + end + end + end + end + + context 'when valid job entries processed' do + before { entry.process! } + + let(:config) do + { rspec: { script: 'rspec' }, + spinach: { script: 'spinach' }, + '.hidden'.to_sym => {} } + end + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq( + rspec: { name: :rspec, + script: %w[rspec], + stage: 'test' }, + spinach: { name: :spinach, + script: %w[spinach], + stage: 'test' }) + end + end + + describe '#descendants' do + it 'creates valid descendant nodes' do + expect(entry.descendants.count).to eq 3 + expect(entry.descendants.first(2)) + .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) + expect(entry.descendants.last) + .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob) + end + end + + describe '#value' do + it 'returns value of visible jobs only' do + expect(entry.value.keys).to eq [:rspec, :spinach] + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb new file mode 100644 index 00000000000..1ab5478dcfa --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/null_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Null do + let(:null) { described_class.new(nil) } + + describe '#leaf?' do + it 'is leaf node' do + expect(null).to be_leaf + end + end + + describe '#valid?' do + it 'is always valid' do + expect(null).to be_valid + end + end + + describe '#errors' do + it 'is does not contain errors' do + expect(null.errors).to be_empty + end + end + + describe '#value' do + it 'returns nil' do + expect(null.value).to eq nil + end + end + + describe '#relevant?' do + it 'is not relevant' do + expect(null.relevant?).to eq false + end + end + + describe '#specified?' do + it 'is not defined' do + expect(null.specified?).to eq false + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb new file mode 100644 index 00000000000..fb9ec70762a --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Stage do + let(:stage) { described_class.new(config) } + + describe 'validations' do + context 'when stage config value is correct' do + let(:config) { 'build' } + + describe '#value' do + it 'returns a stage key' do + expect(stage.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(stage).to be_valid + end + end + end + + context 'when value has a wrong type' do + let(:config) { { test: true } } + + it 'reports errors about wrong type' do + expect(stage.errors) + .to include 'stage config should be a string' + end + end + end + + describe '.default' do + it 'returns default stage' do + expect(described_class.default).to eq 'test' + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/trigger_spec.rb b/spec/lib/gitlab/ci/config/node/trigger_spec.rb new file mode 100644 index 00000000000..a4a3e36754e --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/trigger_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Trigger do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is valid' do + context 'when config is a branch or tag name' do + let(:config) { %w[master feature/branch] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq config + end + end + end + + context 'when config is a regexp' do + let(:config) { ['/^issue-.*$/'] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a special keyword' do + let(:config) { %w[tags triggers branches] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + + context 'when entry value is not valid' do + let(:config) { [1] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'trigger config should be an array of strings or regexps' + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb index 0c6608d906d..2d43e1c1a9d 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb @@ -2,39 +2,31 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Undefined do let(:undefined) { described_class.new(entry) } - let(:entry) { Class.new } - - describe '#leaf?' do - it 'is leaf node' do - expect(undefined).to be_leaf - end - end + let(:entry) { spy('Entry') } describe '#valid?' do - it 'is always valid' do - expect(undefined).to be_valid + it 'delegates method to entry' do + expect(undefined.valid).to eq entry end end describe '#errors' do - it 'is does not contain errors' do - expect(undefined.errors).to be_empty + it 'delegates method to entry' do + expect(undefined.errors).to eq entry end end describe '#value' do - before do - allow(entry).to receive(:default).and_return('some value') - end - - it 'returns default value for entry' do - expect(undefined.value).to eq 'some value' + it 'delegates method to entry' do + expect(undefined.value).to eq entry end end - describe '#undefined?' do - it 'is not a defined entry' do - expect(undefined.defined?).to be false + describe '#specified?' do + it 'is always false' do + allow(entry).to receive(:specified?).and_return(true) + + expect(undefined.specified?).to be false end end end diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb new file mode 100644 index 00000000000..19298e261e3 --- /dev/null +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -0,0 +1,41 @@ +require 'gitlab/email/receiver' + +shared_context :email_shared_context do + let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } + let(:receiver) { Gitlab::Email::Receiver.new(email_raw) } + let(:markdown) { "![image](uploads/image.png)" } + + def setup_attachment + allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( + [ + { + url: "uploads/image.png", + alt: "image", + markdown: markdown + } + ] + ) + end +end + +shared_examples :email_shared_examples do + context "when the user could not be found" do + before do + user.destroy + end + + it "raises a UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when the user is not authorized to the project" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + it "raises a ProjectNotFound" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb new file mode 100644 index 00000000000..e1153154778 --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do + include_context :email_shared_context + it_behaves_like :email_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_new_issue.eml') } + let(:namespace) { create(:namespace, path: 'gitlabhq') } + + let!(:project) { create(:project, :public, namespace: namespace) } + let!(:user) do + create( + :user, + email: 'jake@adventuretime.ooo', + authentication_token: 'auth_token' + ) + end + + context "when everything is fine" do + it "creates a new issue" do + setup_attachment + + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to include('reply by email') + expect(issue.description).to include(markdown) + end + + context "when the reply is blank" do + let(:email_raw) { fixture_file("emails/valid_new_issue_empty.eml") } + + it "creates a new issue" do + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to eq('') + end + end + end + + context "something is wrong" do + context "when the issue could not be saved" do + before do + allow_any_instance_of(Issue).to receive(:persisted?).and_return(false) + end + + it "raises an InvalidIssueError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidIssueError) + end + end + + context "when we can't find the authentication_token" do + let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") } + + it "raises an UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when project is private" do + let(:project) { create(:project, :private, namespace: namespace) } + + it "raises a ProjectNotFound if the user is not a member" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb new file mode 100644 index 00000000000..a2119b0dadf --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do + include_context :email_shared_context + it_behaves_like :email_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_reply.eml') } + let(:project) { create(:project, :public) } + let(:noteable) { create(:issue, project: project) } + let(:user) { create(:user) } + + let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + + context "when the recipient address doesn't include a mail key" do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } + + it "raises a UnknownIncomingEmail" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) + end + end + + context "when no sent notification for the mail key could be found" do + let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } + + it "raises a SentNotificationNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) + end + end + + context "when the email was auto generated" do + let!(:mail_key) { '636ca428858779856c226bb145ef4fad' } + let!(:email_raw) { fixture_file("emails/auto_reply.eml") } + + it "raises an AutoGeneratedEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError) + end + end + + context "when the noteable could not be found" do + before do + noteable.destroy + end + + it "raises a NoteableNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context "when the note could not be saved" do + before do + allow_any_instance_of(Note).to receive(:persisted?).and_return(false) + end + + it "raises an InvalidNoteError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) + end + end + + context "when the reply is blank" do + let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } + + it "raises an EmptyEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) + end + end + + context "when everything is fine" do + before do + setup_attachment + end + + it "creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include("I could not disagree more.") + end + + it "adds all attachments" do + receiver.execute + + note = noteable.notes.last + + expect(note.note).to include(markdown) + end + + context 'when sub-addressing is not supported' do + before do + stub_incoming_email_setting(enabled: true, address: nil) + end + + shared_examples 'an email that contains a mail key' do |header| + it "fetches the mail key from the #{header} header and creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include('I could not disagree more.') + end + end + + context 'mail key is in the References header' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } + + it_behaves_like 'an email that contains a mail key', 'References' + end + end + end +end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 84d2584a791..2a86b427806 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -1,34 +1,14 @@ -require "spec_helper" +require 'spec_helper' +require_relative 'email_shared_blocks' describe Gitlab::Email::Receiver, lib: true do - before do - stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") - stub_config_setting(host: 'localhost') - end - - let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } - let(:email_raw) { fixture_file('emails/valid_reply.eml') } - - let(:project) { create(:project, :public) } - let(:noteable) { create(:issue, project: project) } - let(:user) { create(:user) } - let!(:sent_notification) { SentNotification.record(noteable, user.id, reply_key) } - - let(:receiver) { described_class.new(email_raw) } - - context "when the recipient address doesn't include a reply key" do - let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(reply_key, "") } - - it "raises a SentNotificationNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError) - end - end + include_context :email_shared_context - context "when no sent notificiation for the reply key could be found" do - let(:email_raw) { fixture_file('emails/wrong_reply_key.eml') } + context "when we cannot find a capable handler" do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") } - it "raises a SentNotificationNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError) + it "raises a UnknownIncomingEmail" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end end @@ -36,128 +16,7 @@ describe Gitlab::Email::Receiver, lib: true do let(:email_raw) { "" } it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) - end - end - - context "when the email was auto generated" do - let!(:reply_key) { '636ca428858779856c226bb145ef4fad' } - let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - - it "raises an AutoGeneratedEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError) - end - end - - context "when the user could not be found" do - before do - user.destroy - end - - it "raises a UserNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotFoundError) - end - end - - context "when the user has been blocked" do - before do - user.block - end - - it "raises a UserBlockedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserBlockedError) - end - end - - context "when the user is not authorized to create a note" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end - - it "raises a UserNotAuthorizedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotAuthorizedError) - end - end - - context "when the noteable could not be found" do - before do - noteable.destroy - end - - it "raises a NoteableNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::NoteableNotFoundError) - end - end - - context "when the reply is blank" do - let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - - it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) - end - end - - context "when the note could not be saved" do - before do - allow_any_instance_of(Note).to receive(:persisted?).and_return(false) - end - - it "raises an InvalidNoteError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::InvalidNoteError) - end - end - - context "when everything is fine" do - let(:markdown) { "![image](uploads/image.png)" } - - before do - allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( - [ - { - url: "uploads/image.png", - alt: "image", - markdown: markdown - } - ] - ) - end - - it "creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last - - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include("I could not disagree more.") - end - - it "adds all attachments" do - receiver.execute - - note = noteable.notes.last - - expect(note.note).to include(markdown) - end - - context 'when sub-addressing is not supported' do - before do - stub_incoming_email_setting(enabled: true, address: nil) - end - - shared_examples 'an email that contains a reply key' do |header| - it "fetches the reply key from the #{header} header and creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last - - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include('I could not disagree more.') - end - end - - context 'reply key is in the References header' do - let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } - - it_behaves_like 'an email that contains a reply key', 'References' - end + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index ae064a878b0..8447305a316 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -151,7 +151,13 @@ describe Gitlab::GitAccess, lib: true do def self.run_permission_checks(permissions_matrix) permissions_matrix.keys.each do |role| describe "#{role} access" do - before { project.team << [user, role] } + before do + if role == :admin + user.update_attribute(:admin, true) + else + project.team << [user, role] + end + end permissions_matrix[role].each do |action, allowed| context action do @@ -165,6 +171,17 @@ describe Gitlab::GitAccess, lib: true do end permissions_matrix = { + admin: { + push_new_branch: true, + push_master: true, + push_protected_branch: true, + push_remove_protected_branch: false, + push_tag: true, + push_new_tag: true, + push_all: true, + merge_into_protected_branch: true + }, + master: { push_new_branch: true, push_master: true, @@ -217,19 +234,20 @@ describe Gitlab::GitAccess, lib: true do run_permission_checks(permissions_matrix) end - context "when 'developers can push' is turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_push: true, project: project) } + context "when developers are allowed to push into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end - context "when 'developers can merge' is turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_merge: true, project: project) } + context "developers are allowed to merge into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) } context "when a merge request exists for the given source/target branch" do context "when the merge request is in progress" do before do - create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) + create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', + state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) end run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true })) @@ -242,51 +260,59 @@ describe Gitlab::GitAccess, lib: true do run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) end - end - context "when a merge request does not exist for the given source/target branch" do - run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) + context "when a merge request does not exist for the given source/target branch" do + run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) + end end end - context "when 'developers can merge' and 'developers can push' are turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_merge: true, developers_can_push: true, project: project) } + context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end + + context "when no one is allowed to push to the #{protected_branch_name} protected branch" do + before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } + + run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) + end end + end - describe 'deploy key permissions' do - let(:key) { create(:deploy_key) } - let(:actor) { key } + describe 'deploy key permissions' do + let(:key) { create(:deploy_key) } + let(:actor) { key } - context 'push code' do - subject { access.check('git-receive-pack') } + context 'push code' do + subject { access.check('git-receive-pack') } - context 'when project is authorized' do - before { key.projects << project } + context 'when project is authorized' do + before { key.projects << project } - it { expect(subject).not_to be_allowed } - end + it { expect(subject).not_to be_allowed } + end - context 'when unauthorized' do - context 'to public project' do - let(:project) { create(:project, :public) } + context 'when unauthorized' do + context 'to public project' do + let(:project) { create(:project, :public) } - it { expect(subject).not_to be_allowed } - end + it { expect(subject).not_to be_allowed } + end - context 'to internal project' do - let(:project) { create(:project, :internal) } + context 'to internal project' do + let(:project) { create(:project, :internal) } - it { expect(subject).not_to be_allowed } - end + it { expect(subject).not_to be_allowed } + end - context 'to private project' do - let(:project) { create(:project, :internal) } + context 'to private project' do + let(:project) { create(:project, :internal) } - it { expect(subject).not_to be_allowed } - end + it { expect(subject).not_to be_allowed } end end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 364532e94e3..fc021416d92 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -17,6 +17,18 @@ describe Gitlab::Highlight, lib: true do expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) end + + describe 'with CRLF' do + let(:branch) { 'crlf-diff' } + let(:blob) { repository.blob_at_branch(branch, path) } + let(:lines) do + Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace') + end + + it 'strips extra LFs' do + expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>") + end + end end describe 'custom highlighting from .gitattributes' do diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index b1a5d72c624..b5550ca1963 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -18,7 +18,6 @@ "position": 0, "branch_name": null, "description": "Aliquam enim illo et possimus.", - "milestone_id": 18, "state": "opened", "iid": 10, "updated_by_id": null, @@ -27,6 +26,52 @@ "due_date": null, "moved_to_id": null, "test_ee_field": "test", + "milestone": { + "id": 1, + "title": "v0.0", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "events": [ + { + "id": 487, + "target_type": "Milestone", + "target_id": 1, + "title": null, + "data": null, + "project_id": 46, + "created_at": "2016-06-14T15:02:04.418Z", + "updated_at": "2016-06-14T15:02:04.418Z", + "action": 1, + "author_id": 18 + } + ] + }, + "label_links": [ + { + "id": 2, + "label_id": 2, + "target_id": 3, + "target_type": "Issue", + "created_at": "2016-07-22T08:57:02.840Z", + "updated_at": "2016-07-22T08:57:02.840Z", + "label": { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "priority": null + } + } + ], "notes": [ { "id": 351, @@ -233,7 +278,6 @@ "position": 0, "branch_name": null, "description": "Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.", - "milestone_id": 16, "state": "opened", "iid": 9, "updated_by_id": null, @@ -447,7 +491,6 @@ "position": 0, "branch_name": null, "description": "Ea recusandae neque autem tempora.", - "milestone_id": 16, "state": "closed", "iid": 8, "updated_by_id": null, @@ -661,7 +704,6 @@ "position": 0, "branch_name": null, "description": "Maiores architecto quos in dolorem.", - "milestone_id": 17, "state": "opened", "iid": 7, "updated_by_id": null, @@ -875,7 +917,6 @@ "position": 0, "branch_name": null, "description": "Ut aut ut et tenetur velit aut id modi.", - "milestone_id": 16, "state": "opened", "iid": 6, "updated_by_id": null, @@ -1089,7 +1130,6 @@ "position": 0, "branch_name": null, "description": "Dicta nisi nihil non ipsa velit.", - "milestone_id": 20, "state": "closed", "iid": 5, "updated_by_id": null, @@ -1303,7 +1343,6 @@ "position": 0, "branch_name": null, "description": "Ut et explicabo vel voluptatem consequuntur ut sed.", - "milestone_id": 19, "state": "closed", "iid": 4, "updated_by_id": null, @@ -1517,7 +1556,6 @@ "position": 0, "branch_name": null, "description": "Non asperiores velit accusantium voluptate.", - "milestone_id": 18, "state": "closed", "iid": 3, "updated_by_id": null, @@ -1731,7 +1769,6 @@ "position": 0, "branch_name": null, "description": "Molestiae corporis magnam et fugit aliquid nulla quia.", - "milestone_id": 17, "state": "closed", "iid": 2, "updated_by_id": null, @@ -1945,7 +1982,6 @@ "position": 0, "branch_name": null, "description": "Quod ad architecto qui est sed quia.", - "milestone_id": 20, "state": "closed", "iid": 1, "updated_by_id": null, @@ -2259,117 +2295,6 @@ "author_id": 25 } ] - }, - { - "id": 18, - "title": "v2.0", - "project_id": 5, - "description": "Error dolorem rerum aut nulla.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.576Z", - "updated_at": "2016-06-14T15:02:04.576Z", - "state": "active", - "iid": 3, - "events": [ - { - "id": 242, - "target_type": "Milestone", - "target_id": 18, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.579Z", - "updated_at": "2016-06-14T15:02:04.579Z", - "action": 1, - "author_id": 1 - }, - { - "id": 58, - "target_type": "Milestone", - "target_id": 18, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.579Z", - "updated_at": "2016-06-14T15:02:04.579Z", - "action": 1, - "author_id": 22 - } - ] - }, - { - "id": 17, - "title": "v1.0", - "project_id": 5, - "description": "Molestiae perspiciatis voluptates doloremque commodi veniam consequatur.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.569Z", - "updated_at": "2016-06-14T15:02:04.569Z", - "state": "active", - "iid": 2, - "events": [ - { - "id": 243, - "target_type": "Milestone", - "target_id": 17, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.570Z", - "updated_at": "2016-06-14T15:02:04.570Z", - "action": 1, - "author_id": 1 - }, - { - "id": 57, - "target_type": "Milestone", - "target_id": 17, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.570Z", - "updated_at": "2016-06-14T15:02:04.570Z", - "action": 1, - "author_id": 20 - } - ] - }, - { - "id": 16, - "title": "v0.0", - "project_id": 5, - "description": "Velit numquam et sed sit.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.561Z", - "updated_at": "2016-06-14T15:02:04.561Z", - "state": "closed", - "iid": 1, - "events": [ - { - "id": 244, - "target_type": "Milestone", - "target_id": 16, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.563Z", - "updated_at": "2016-06-14T15:02:04.563Z", - "action": 1, - "author_id": 26 - }, - { - "id": 56, - "target_type": "Milestone", - "target_id": 16, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.563Z", - "updated_at": "2016-06-14T15:02:04.563Z", - "action": 1, - "author_id": 26 - } - ] } ], "snippets": [ @@ -2471,7 +2396,6 @@ "title": "Cannot be automatically merged", "created_at": "2016-06-14T15:02:36.568Z", "updated_at": "2016-06-14T15:02:56.815Z", - "milestone_id": null, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -2909,7 +2833,6 @@ "title": "Can be automatically merged", "created_at": "2016-06-14T15:02:36.418Z", "updated_at": "2016-06-14T15:02:57.013Z", - "milestone_id": null, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3194,7 +3117,6 @@ "title": "Qui accusantium et inventore facilis doloribus occaecati officiis.", "created_at": "2016-06-14T15:02:25.168Z", "updated_at": "2016-06-14T15:02:59.521Z", - "milestone_id": 17, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3479,7 +3401,6 @@ "title": "In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.", "created_at": "2016-06-14T15:02:24.760Z", "updated_at": "2016-06-14T15:02:59.749Z", - "milestone_id": 20, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -4170,7 +4091,6 @@ "title": "Voluptates consequatur eius nemo amet libero animi illum delectus tempore.", "created_at": "2016-06-14T15:02:24.415Z", "updated_at": "2016-06-14T15:02:59.958Z", - "milestone_id": 17, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -4719,7 +4639,6 @@ "title": "In a rerum harum nihil accusamus aut quia nobis non.", "created_at": "2016-06-14T15:02:24.000Z", "updated_at": "2016-06-14T15:03:00.225Z", - "milestone_id": 19, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5219,7 +5138,6 @@ "title": "Corporis provident similique perspiciatis dolores eos animi.", "created_at": "2016-06-14T15:02:23.767Z", "updated_at": "2016-06-14T15:03:00.475Z", - "milestone_id": 18, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5480,7 +5398,6 @@ "title": "Eligendi reprehenderit doloribus quia et sit id.", "created_at": "2016-06-14T15:02:23.014Z", "updated_at": "2016-06-14T15:03:00.685Z", - "milestone_id": 20, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -6171,7 +6088,6 @@ "title": "Et ipsam voluptas velit sequi illum ut.", "created_at": "2016-06-14T15:02:22.825Z", "updated_at": "2016-06-14T15:03:00.904Z", - "milestone_id": 16, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 6ae20c943b1..32c0d6462f1 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -60,6 +60,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) end + + it 'has labels associated to label links, associated to issues' do + restored_project_json + + expect(Label.first.label_links.first.target).not_to be_nil + end + + it 'has milestones associated to issues' do + restored_project_json + + expect(Milestone.find_by_description('test milestone').issues).not_to be_empty + end end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 057ef6e76a0..3a86a4ce07c 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -31,10 +31,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json).to include({ "visibility_level" => 20 }) end - it 'has events' do - expect(saved_project_json['milestones'].first['events']).not_to be_empty - end - it 'has milestones' do expect(saved_project_json['milestones']).not_to be_empty end @@ -43,8 +39,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests']).not_to be_empty end - it 'has labels' do - expect(saved_project_json['labels']).not_to be_empty + it 'has merge request\'s milestones' do + expect(saved_project_json['merge_requests'].first['milestone']).not_to be_empty + end + + it 'has events' do + expect(saved_project_json['merge_requests'].first['milestone']['events']).not_to be_empty end it 'has snippets' do @@ -103,6 +103,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['pipelines'].first['notes']).not_to be_empty end + it 'has labels with no associations' do + expect(saved_project_json['labels']).not_to be_empty + end + + it 'has labels associated to records' do + expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty + end + it 'does not complain about non UTF-8 characters in MR diffs' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -113,19 +121,19 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do def setup_project issue = create(:issue, assignee: user) - label = create(:label) snippet = create(:project_snippet) release = create(:release) project = create(:project, :public, issues: [issue], - labels: [label], snippets: [snippet], releases: [release] ) - - merge_request = create(:merge_request, source_project: project) + label = create(:label, project: project) + create(:label_link, label: label, target: issue) + milestone = create(:milestone, project: project) + merge_request = create(:merge_request, source_project: project, milestone: milestone) commit_status = create(:commit_status, project: project) ci_pipeline = create(:ci_pipeline, @@ -135,7 +143,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do statuses: [commit_status]) create(:ci_build, pipeline: ci_pipeline, project: project) - milestone = create(:milestone, project: project) + create(:milestone, project: project) create(:note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index afb3e26f8fb..1dcf2c0668b 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -43,9 +43,9 @@ describe Gitlab::IncomingEmail, lib: true do end end - context 'self.key_from_fallback_reply_message_id' do + context 'self.key_from_fallback_message_id' do it 'returns reply key' do - expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key') + expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') end end end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 8809b7e3f12..d88bcae41fb 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -39,6 +39,12 @@ describe Gitlab::Metrics::Instrumentation do allow(@dummy).to receive(:name).and_return('Dummy') end + describe '.series' do + it 'returns a String' do + expect(described_class.series).to be_an_instance_of(String) + end + end + describe '.configure' do it 'yields self' do described_class.configure do |c| @@ -78,8 +84,7 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:measure_method). - with('Dummy.foo') + expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @dummy.foo end @@ -157,8 +162,7 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:measure_method). - with('Dummy#bar') + expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @dummy.new.bar end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index cf0e282c2fb..9e2ea89a712 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -28,20 +28,20 @@ describe Gitlab::Metrics::System do end describe '.cpu_time' do - it 'returns a Float' do - expect(described_class.cpu_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.cpu_time).to be_an_instance_of(Fixnum) end end describe '.real_time' do - it 'returns a Float' do - expect(described_class.real_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.real_time).to be_an_instance_of(Fixnum) end end describe '.monotonic_time' do - it 'returns a Float' do - expect(described_class.monotonic_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.monotonic_time).to be_an_instance_of(Fixnum) end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 3b1c67a2147..f1a191d9410 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -46,19 +46,11 @@ describe Gitlab::Metrics::Transaction do end end - describe '#measure_method' do - it 'adds a new method if it does not exist already' do - transaction.measure_method('Foo#bar') { 'foo' } + describe '#method_call_for' do + it 'returns a MethodCall' do + method = transaction.method_call_for('Foo#bar') - expect(transaction.methods['Foo#bar']). - to be_an_instance_of(Gitlab::Metrics::MethodCall) - end - - it 'adds timings to an existing method call' do - transaction.measure_method('Foo#bar') { 'foo' } - transaction.measure_method('Foo#bar') { 'foo' } - - expect(transaction.methods['Foo#bar'].call_count).to eq(2) + expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall) end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 96f7eabbca6..84f9475a0f8 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -147,4 +147,10 @@ describe Gitlab::Metrics do end end end + + describe '#series_prefix' do + it 'returns a String' do + expect(described_class.series_prefix).to be_an_instance_of(String) + end + end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index aa9ec243498..5bb095366fa 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::UserAccess, lib: true do describe 'push to protected branch if allowed for developers' do before do - @branch = create :protected_branch, project: project, developers_can_push: true + @branch = create :protected_branch, :developers_can_push, project: project end it 'returns true if user is a master' do @@ -65,7 +65,7 @@ describe Gitlab::UserAccess, lib: true do describe 'merge to protected branch if allowed for developers' do before do - @branch = create :protected_branch, project: project, developers_can_merge: true + @branch = create :protected_branch, :developers_can_merge, project: project end it 'returns true if user is a master' do diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index cd5f40fe3d2..853f6943cef 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -170,4 +170,52 @@ describe Ability, lib: true do end end end + + describe '.issues_readable_by_user' do + context 'with an admin user' do + it 'returns all given issues' do + user = build(:user, admin: true) + issue = build(:issue) + + expect(described_class.issues_readable_by_user([issue], user)). + to eq([issue]) + end + end + + context 'with a regular user' do + it 'returns the issues readable by the user' do + user = build(:user) + issue = build(:issue) + + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(described_class.issues_readable_by_user([issue], user)). + to eq([issue]) + end + + it 'returns an empty Array when no issues are readable' do + user = build(:user) + issue = build(:issue) + + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(described_class.issues_readable_by_user([issue], user)).to eq([]) + end + end + + context 'without a regular user' do + it 'returns issues that are publicly visible' do + hidden_issue = build(:issue) + visible_issue = build(:issue) + + expect(hidden_issue).to receive(:publicly_visible?).and_return(false) + expect(visible_issue).to receive(:publicly_visible?).and_return(true) + + issues = described_class. + issues_readable_by_user([hidden_issue, visible_issue]) + + expect(issues).to eq([visible_issue]) + end + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ec1544bf815..c3392ee7440 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -13,6 +13,26 @@ describe Commit, models: true do it { is_expected.to include_module(StaticModel) } end + describe '#author' do + it 'looks up the author in a case-insensitive way' do + user = create(:user, email: commit.author_email.upcase) + expect(commit.author).to eq(user) + end + + it 'caches the author' do + user = create(:user, email: commit.author_email) + expect(RequestStore).to receive(:active?).twice.and_return(true) + expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original + + expect(commit.author).to eq(user) + key = "commit_author:#{commit.author_email}" + expect(RequestStore.store[key]).to eq(user) + + expect(commit.author).to eq(user) + RequestStore.store.clear + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(commit.to_reference).to eq commit.id diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 5e652660e2c..549b0042038 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -68,7 +68,7 @@ describe Issue, "Mentionable" do describe '#create_cross_references!' do let(:project) { create(:project) } - let(:author) { double('author') } + let(:author) { build(:user) } let(:commit) { project.commit } let(:commit2) { project.commit } @@ -88,6 +88,10 @@ describe Issue, "Mentionable" do let(:author) { create(:author) } let(:issues) { create_list(:issue, 2, project: project, author: author) } + before do + project.team << [author, Gitlab::Access::DEVELOPER] + end + context 'before changes are persisted' do it 'ignores pre-existing references' do issue = create_issue(description: issues[0].to_reference) diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index af8e890ca95..1fa96eb1f15 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -119,7 +119,7 @@ describe DiffNote, models: true do context "when the merge request's diff refs don't match that of the diff note" do before do - allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) + allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs) end it "returns false" do @@ -168,7 +168,7 @@ describe DiffNote, models: true do context "when the note is outdated" do before do - allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) + allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs) end it "uses the DiffPositionUpdateService" do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 7629af6a570..8a84ac0a7c7 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -11,4 +11,23 @@ describe Environment, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_within(0..255) } + + it { is_expected.to validate_length_of(:external_url).is_within(0..255) } + + # To circumvent a not null violation of the name column: + # https://github.com/thoughtbot/shoulda-matchers/issues/336 + it 'validates uniqueness of :external_url' do + create(:environment) + + is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) + end + + describe '#nullify_external_url' do + it 'replaces a blank url with nil' do + env = build(:environment, external_url: "") + + expect(env.save).to be true + expect(env.external_url).to be_nil + end + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 6a897c96690..3259f795296 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -306,4 +306,257 @@ describe Issue, models: true do expect(user2.assigned_open_issues_count).to eq(1) end end + + describe '#visible_to_user?' do + context 'with a user' do + let(:user) { build(:user) } + let(:issue) { build(:issue) } + + it 'returns true when the issue is readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(issue.visible_to_user?(user)).to eq(true) + end + + it 'returns false when the issue is not readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(issue.visible_to_user?(user)).to eq(false) + end + end + + context 'without a user' do + let(:issue) { build(:issue) } + + it 'returns true when the issue is publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(true) + + expect(issue.visible_to_user?).to eq(true) + end + + it 'returns false when the issue is not publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(false) + + expect(issue.visible_to_user?).to eq(false) + end + end + end + + describe '#readable_by?' do + describe 'with a regular user that is not a team member' do + let(:user) { create(:user) } + + context 'using a public project' do + let(:project) { create(:empty_project, :public) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, project: project, confidential: true) + + expect(issue).not_to be_readable_by(user) + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + context 'using an internal user' do + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + + context 'using an external user' do + before do + allow(user).to receive(:external?).and_return(true) + end + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + + context 'when the user is the project owner' do + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + end + end + + context 'with a regular user that is a team member' do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + context 'using a public project' do + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + end + + context 'with an admin user' do + let(:project) { create(:empty_project) } + let(:user) { create(:user, admin: true) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + end + + describe '#publicly_visible?' do + context 'using a public project' do + let(:project) { create(:empty_project, :public) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8ad7ab3e7f..21d22c776e9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -65,11 +65,11 @@ describe MergeRequest, models: true do end describe '#target_branch_sha' do - context 'when the target branch does not exist anymore' do - let(:project) { create(:project) } + let(:project) { create(:project) } - subject { create(:merge_request, source_project: project, target_project: project) } + subject { create(:merge_request, source_project: project, target_project: project) } + context 'when the target branch does not exist' do before do project.repository.raw_repository.delete_branch(subject.target_branch) end @@ -78,6 +78,12 @@ describe MergeRequest, models: true do expect(subject.target_branch_sha).to be_nil end end + + it 'returns memoized value' do + subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7' + + expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' + end end describe '#source_branch_sha' do @@ -103,6 +109,12 @@ describe MergeRequest, models: true do expect(subject.source_branch_sha).to be_nil end end + + it 'returns memoized value' do + subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + + expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end end describe '#to_reference' do @@ -674,4 +686,28 @@ describe MergeRequest, models: true do subject.reload_diff end end + + describe "#diff_sha_refs" do + context "with diffs" do + subject { create(:merge_request, :with_diffs) } + + it "does not touch the repository" do + subject # Instantiate the object + + expect_any_instance_of(Repository).not_to receive(:commit) + + subject.diff_sha_refs + end + + it "returns expected diff_refs" do + expected_diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: subject.merge_request_diff.base_commit_sha, + start_sha: subject.merge_request_diff.start_commit_sha, + head_sha: subject.merge_request_diff.head_commit_sha + ) + + expect(subject.diff_sha_refs).to eq(expected_diff_refs) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index be5c53c44be..e365e4e98b2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -245,6 +245,34 @@ describe Project, models: true do end end + describe "#new_issue_address" do + let(:project) { create(:empty_project, path: "somewhere") } + let(:user) { create(:user) } + + context 'incoming email enabled' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + end + + it 'returns the address to create a new issue' do + token = user.authentication_token + address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab" + + expect(project.new_issue_address(user)).to eq(address) + end + end + + context 'incoming email disabled' do + before do + stub_incoming_email_setting(enabled: false) + end + + it 'returns nil' do + expect(project.new_issue_address(user)).to be_nil + end + end + end + describe 'last_activity methods' do let(:project) { create(:project) } let(:last_event) { double(created_at: Time.now) } @@ -1067,46 +1095,6 @@ describe Project, models: true do end end - describe "#developers_can_push_to_protected_branch?" do - let(:project) { create(:empty_project) } - - context "when the branch matches a protected branch via direct match" do - it "returns true if 'Developers can Push' is turned on" do - create(:protected_branch, name: "production", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('production')).to be true - end - - it "returns false if 'Developers can Push' is turned off" do - create(:protected_branch, name: "production", project: project, developers_can_push: false) - - expect(project.developers_can_push_to_protected_branch?('production')).to be false - end - end - - context "when the branch matches a protected branch via wilcard match" do - it "returns true if 'Developers can Push' is turned on" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true - end - - it "returns false if 'Developers can Push' is turned off" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: false) - - expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false - end - end - - context "when the branch does not match a protected branch" do - it "returns false" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false - end - end - end - describe '#container_registry_path_with_namespace' do let(:project) { create(:empty_project, path: 'PROJECT') } diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 1f42fbd3385..5eaf0d3b7a6 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -199,48 +199,69 @@ describe ProjectTeam, models: true do end end - describe "#max_member_access_for_users" do - it 'returns correct roles for different users' do - master = create(:user) - reporter = create(:user) - promoted_guest = create(:user) - guest = create(:user) - project = create(:project) + shared_examples_for "#max_member_access_for_users" do |enable_request_store| + describe "#max_member_access_for_users" do + before do + RequestStore.begin! if enable_request_store + end - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [promoted_guest, :guest] - project.team << [guest, :guest] + after do + if enable_request_store + RequestStore.end! + RequestStore.clear! + end + end - group = create(:group) - group_developer = create(:user) - second_developer = create(:user) - project.project_group_links.create( - group: group, - group_access: Gitlab::Access::DEVELOPER) - - group.add_master(promoted_guest) - group.add_developer(group_developer) - group.add_developer(second_developer) - - second_group = create(:group) - project.project_group_links.create( - group: second_group, - group_access: Gitlab::Access::MASTER) - second_group.add_master(second_developer) - - users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) - - expected = { - master.id => Gitlab::Access::MASTER, - reporter.id => Gitlab::Access::REPORTER, - promoted_guest.id => Gitlab::Access::DEVELOPER, - guest.id => Gitlab::Access::GUEST, - group_developer.id => Gitlab::Access::DEVELOPER, - second_developer.id => Gitlab::Access::MASTER - } - - expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + it 'returns correct roles for different users' do + master = create(:user) + reporter = create(:user) + promoted_guest = create(:user) + guest = create(:user) + project = create(:project) + + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [promoted_guest, :guest] + project.team << [guest, :guest] + + group = create(:group) + group_developer = create(:user) + second_developer = create(:user) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER) + + group.add_master(promoted_guest) + group.add_developer(group_developer) + group.add_developer(second_developer) + + second_group = create(:group) + project.project_group_links.create( + group: second_group, + group_access: Gitlab::Access::MASTER) + second_group.add_master(second_developer) + + users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) + + expected = { + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER, + promoted_guest.id => Gitlab::Access::DEVELOPER, + guest.id => Gitlab::Access::GUEST, + group_developer.id => Gitlab::Access::DEVELOPER, + second_developer.id => Gitlab::Access::MASTER + } + + expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + end end end + + describe '#max_member_access_for_users with RequestStore' do + it_behaves_like "#max_member_access_for_users", true + end + + describe '#max_member_access_for_users without RequestStore' do + it_behaves_like "#max_member_access_for_users", false + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 5bc1bd9a930..cce15538b93 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -50,8 +50,9 @@ describe Repository, models: true do double_first = double(committed_date: Time.now) double_last = double(committed_date: Time.now - 1.second) - allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first) - allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last) + allow(tag_a).to receive(:target).and_return(double_first) + allow(tag_b).to receive(:target).and_return(double_last) + allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } @@ -64,8 +65,9 @@ describe Repository, models: true do double_first = double(committed_date: Time.now - 1.second) double_last = double(committed_date: Time.now) - allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last) - allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first) + allow(tag_a).to receive(:target).and_return(double_last) + allow(tag_b).to receive(:target).and_return(double_first) + allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } @@ -381,9 +383,13 @@ describe Repository, models: true do end describe '#rm_branch' do + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:blank_sha) { '0000000000000000000000000000000000000000' } + context 'when pre hooks were successful' do it 'should run without errors' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + expect_any_instance_of(GitHooksService).to receive(:execute). + with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -418,10 +424,13 @@ describe Repository, models: true do end describe '#commit_with_hooks' do + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + context 'when pre hooks were successful' do before do expect_any_instance_of(GitHooksService).to receive(:execute). - and_return(true) + with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature'). + and_yield.and_return(true) end it 'should run without errors' do @@ -435,6 +444,14 @@ describe Repository, models: true do repository.commit_with_hooks(user, 'feature') { sample_commit.id } end + + context "when the branch wasn't empty" do + it 'updates the head' do + expect(repository.find_branch('feature').target.id).to eq(old_rev) + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + expect(repository.find_branch('feature').target.id).to eq(sample_commit.id) + end + end end context 'when pre hooks failed' do @@ -1198,17 +1215,6 @@ describe Repository, models: true do end end - describe '#local_branches' do - it 'returns the local branches' do - masterrev = repository.find_branch('master').target - create_remote_branch('joe', 'remote_branch', masterrev) - repository.add_branch(user, 'local_branch', masterrev) - - expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) - expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) - end - end - describe "#keep_around" do it "does not fail if we attempt to reference bad commit" do expect(repository.kept_around?('abc1234')).to be_falsey @@ -1236,9 +1242,4 @@ describe Repository, models: true do File.delete(path) end end - - def create_remote_branch(remote_name, branch_name, target) - rugged = repository.rugged - rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) - end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 719da27f919..e8fd697965f 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -112,7 +112,7 @@ describe API::API, api: true do before do project.repository.add_branch(user, protected_branch, 'master') - create(:protected_branch, project: project, name: protected_branch, developers_can_push: true, developers_can_merge: true) + create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch) end it 'updates that a developer can push' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 5219c808791..e4ea8506598 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -73,9 +73,13 @@ describe API::API, api: true do context "authorized user" do it "should return a commit by sha" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['id']).to eq(project.repository.commit.id) expect(json_response['title']).to eq(project.repository.commit.title) + expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) + expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) end it "should return a 404 error if not found" do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb new file mode 100644 index 00000000000..05e57905343 --- /dev/null +++ b/spec/requests/api/environments_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:project, :private, namespace: user.namespace) } + let!(:environment) { create(:environment, project: project) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/environments' do + context 'as member of the project' do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/environments", user) } + end + + it 'returns project environments' do + get api("/projects/#{project.id}/environments", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['name']).to eq(environment.name) + expect(json_response.first['external_url']).to eq(environment.external_url) + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/environments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /projects/:id/environments' do + context 'as a member' do + it 'creates a environment with valid params' do + post api("/projects/#{project.id}/environments", user), name: "mepmep" + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('mepmep') + expect(json_response['external']).to be nil + end + + it 'requires name to be passed' do + post api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com' + + expect(response).to have_http_status(400) + end + + it 'returns a 400 if environment already exists' do + post api("/projects/#{project.id}/environments", user), name: environment.name + + expect(response).to have_http_status(400) + end + end + + context 'a non member' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com' + + expect(response).to have_http_status(404) + end + + it 'returns a 400 when the required params are missing' do + post api("/projects/12345/environments", non_member), external_url: 'http://env.git.com' + end + end + end + + describe 'PUT /projects/:id/environments/:environment_id' do + it 'returns a 200 if name and external_url are changed' do + url = 'https://mepmep.whatever.ninja' + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep', external_url: url + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it "won't update the external_url if only the name is passed" do + url = environment.external_url + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it 'returns a 404 if the environment does not exist' do + put api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE /projects/:id/environments/:environment_id' do + context 'as a master' do + it 'returns a 200 for an existing environment' do + delete api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 404 for non existing id' do + delete api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + context 'a non member' do + it 'rejects the request' do + delete api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 12f2cfa6942..9d3d28e0b91 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -531,10 +531,8 @@ describe API::API, api: true do describe 'POST /projects/:id/issues with spam filtering' do before do - Grape::Endpoint.before_each do |endpoint| - allow(endpoint).to receive(:check_for_spam?).and_return(true) - allow(endpoint).to receive(:is_spam?).and_return(true) - end + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) end let(:params) do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 47c0580e0f0..ffa998dffc3 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -7,6 +7,7 @@ describe GitPushService, services: true do let(:project) { create :project } before do + project.team << [user, :master] @blankrev = Gitlab::Git::BLANK_SHA @oldrev = sample_commit.parent_id @newrev = sample_commit.id @@ -172,7 +173,7 @@ describe GitPushService, services: true do describe "Push Event" do before do service = execute_service(project, user, @oldrev, @newrev, @ref ) - @event = Event.last + @event = Event.find_by_action(Event::PUSHED) @push_data = service.push_data end @@ -224,8 +225,10 @@ describe GitPushService, services: true do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false, developers_can_merge: false }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER) + expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::MASTER) end it "when pushing a branch for the first time with default branch protection disabled" do @@ -233,8 +236,8 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).not_to receive(:create) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).to be_empty end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do @@ -242,9 +245,12 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true, developers_can_merge: false }) - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.last.push_access_level.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(project.protected_branches.last.merge_access_level.access_level).to eq(Gitlab::Access::MASTER) end it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do @@ -252,8 +258,10 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false, developers_can_merge: true }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER) + expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::DEVELOPER) end it "when pushing new commits to existing branch" do diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index f5bf3c1e367..8ffebcac698 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -75,6 +75,17 @@ describe MergeRequests::MergeService, services: true do expect(merge_request.merge_error).to eq("error") end + + it 'aborts if there is a merge conflict' do + allow_any_instance_of(Repository).to receive(:merge).and_return(false) + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request.open?).to be_truthy + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to eq("Conflicts detected during merge") + end end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index ce643b3f860..781ee7ffed3 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -57,7 +57,7 @@ describe MergeRequests::RefreshService, services: true do it 'should execute hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update') + with(@merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).not_to be_empty } @@ -113,7 +113,7 @@ describe MergeRequests::RefreshService, services: true do it 'should execute hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).to be_empty } @@ -158,7 +158,7 @@ describe MergeRequests::RefreshService, services: true do it 'refreshes the merge request' do expect(refresh_service).to receive(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') diff --git a/spec/support/issue_helpers.rb b/spec/support/issue_helpers.rb new file mode 100644 index 00000000000..85241793743 --- /dev/null +++ b/spec/support/issue_helpers.rb @@ -0,0 +1,13 @@ +module IssueHelpers + def visit_issues(project, opts = {}) + visit namespace_project_issues_path project.namespace, project, opts + end + + def first_issue + page.all('ul.issues-list > li').first.text + end + + def last_issue + page.all('ul.issues-list > li').last.text + end +end diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb new file mode 100644 index 00000000000..d5801c8272f --- /dev/null +++ b/spec/support/merge_request_helpers.rb @@ -0,0 +1,13 @@ +module MergeRequestHelpers + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def first_merge_request + page.all('ul.mr-list > li').first.text + end + + def last_merge_request + page.all('ul.mr-list > li').last.text + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 3735abe2302..4561aa9644d 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -21,7 +21,8 @@ module TestEnv 'expand-collapse-diffs' => '4842455', 'expand-collapse-files' => '025db92', 'expand-collapse-lines' => '238e82d', - 'video' => '8879059' + 'video' => '8879059', + 'crlf-diff' => '5938907' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 69b2b9b6d5b..1a3bbb9c8cc 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -38,7 +38,7 @@ Teaspoon.configure do |config| # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" + suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}" # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. # suite.javascripts = [] diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb new file mode 100644 index 00000000000..78af61f15a7 --- /dev/null +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'projects/issues/_related_branches' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:branch) { project.repository.find_branch('feature') } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') } + + before do + assign(:project, project) + assign(:related_branches, ['feature']) + + render + end + + it 'shows the related branches with their build status' do + expect(rendered).to match('feature') + expect(rendered).to have_css('.related-branch-ci-status') + end +end diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb index de40a6f78af..fe70501eeac 100644 --- a/spec/workers/email_receiver_worker_spec.rb +++ b/spec/workers/email_receiver_worker_spec.rb @@ -17,7 +17,7 @@ describe EmailReceiverWorker do context "when an error occurs" do before do - allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::Receiver::EmptyEmailError) + allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::EmptyEmailError) end it "sends out a rejection email" do |